As your application grows, things get more complicated. It gets harder (and more time consuming) to check that everything works OK every time a change is made. The risk of breaking another part of the application while developing new features also increases along with your application’s size. Manual testing might not be enough and it becomes easier to skip over basic practices like ensuring a key feature still works.
That’s where testing your code comes to the rescue. Testing helps mitigate those risks, resulting in a more efficient way of using your team’s time. Testing also improves the way your code is designed, making your code easier to maintain and shortening the ramp-up time for new developers.
What is a Code Test?
Code tests are just code that tests your code! It’s code running your application (or part of it) and verifying that it behaves as expected, checking that given an input it returns the right output according to the specifications.
Let’s say that we have a function that, given an integer, it returns the square root of that integer and we want to test if it’s working ok. We would write some code that uses a function sending a 9 and checking that it returns 3, sending a 4 and checking that it returns 2 and sending a negative number and checking that it fails throwing the right exception or showing the right error message according to the specifications.
Why Should You Test Your Code?
Writing tests for your code is your first line of defence for preventing issues during later stages of the development process and even on production environment. However, that’s not the only benefit your team (and your business) get:
- Testing helps your team build better-designed and more modular components, which results in easier to maintain code. This means less time spent on fixing each bug or adding a new feature. It also reduces ramp-up time for developers getting on board.
- Testing acts as a safety net preventing you from creating new issues in existing features when you fix a bug or when you develop a new feature.
- Testing serves as documentation for other developers about what each component is meant to do. This also improves ramp-up time.
It is a fact that a bug detected during the development phase is by far easier to tackle than a bug detected in a later stage. That’s because the developer has a fresh perspective on the component being created/updated and knows exactly which changeset produced that issue.
If the issue was detected on the production environment is that the developer would have to stop working on the feature he’s working on at that moment, perform the context switch, do some research to find the cause of the bug (this can go from a few minutes to several days) and go through the entire QA process again before the fix reaches the production environment.
Believe me, as a developer, context switches are something you want to avoid as much as possible when developing a feature. Not only do they take up a good amount of time for preparing your brain for fixing the issue but also for working back on the feature that was interrupted.
So, is it Convenient to Have my Code Tested?
If you write code tests for each feature you’re building, that would imply that each task will take some extra time before its development phase is considered finished (in comparison to not writing tests at all).
If you put the focus in the short term, you might see this extra time as a cost but If you take a look at the mid/long term you’ll see that it’s actually an investment. The time dedicated to writing tests is by far exceeded by the time it would take to fix bugs that would have been prevented by these tests.
There’s a really good article by Eric Elliot that demonstrates this. The amount of time saved has a direct relation to how big your application is going to be.
As I mentioned before, ramp-up time also decreases. That’s not only good news for your wallet but it also provides you with more flexibility in case you need to scale up your team.
Then consider the (harder to measure) harm on the company’s reputation when an issue is found by an end user, along with the customer support-associated costs and the risk of having an entire feature unavailable in production until the fix is ready.
Types of Code Tests
There are several types of automated tests and each one of them serves a different purpose:
- Unit tests focus on ensuring that an isolated component of the application works as expected. This is achieved by faking component dependencies.
- Integration tests ensure that several components work together as expected. Apart from testing components’ public members, it’s a common scenario to test for side-effects (such as database I/O or logging).
- Functional/e2e tests ensure that everything works as expected by simulating a real user interaction with the application. They primarily test what the user sees.
You shouldn’t choose one type of test over the other. Rather, you should use a mix of them at different moments in your development process. It’s probably a good idea to create separate folders/subprojects for each type of test so they are easier to identify.
Since unit tests don’t involve the component dependencies, they usually take just a few seconds to execute. Therefore, it’s a good idea to add them to your compilation process (if you are using a compiled language). Alternatively, you can have a background process watching all the code changes and running them to ensure you’re not accidentally breaking any part of the application (if you are using an interpreted language).
On the other hand, integration and functional tests often involve time-consuming operations like writing or reading records from a database, sending an email or calling a third-party API (among others).
Because of that, it’s not practical to have them running every time a change is made. For this reason, it’s better to configure your continuous integration tool to execute them each time a commit is pushed to a branch. That way, you’ll avoid integrating that code to your main branch if any of the tests fail.
What Makes a Good Test Suite?
The first indicator you should pay attention to is your test coverage. You should usually aim to cover levels between 80% and 90% of your code, but coverage alone is not a guarantee of having a good level of tests. You can still get a good coverage level writing low quality tests. I would be even more suspicious of levels near 100% since that could indicate someone writing tests just to get a happy indicator without thinking too much about what they’re doing.
I like to use the Arrange-Act-Assert pattern for keeping my tests organized. The arrange section contains all the setup needed before being able to actually invoke the method we are testing (here’s where you’d configure all your fake dependencies). The act section contains the actual call to the method being tested. The assert section contains all the aspects you want to check to decide if things are working correctly or not (by comparing the actual results to the expected results).
The name/description of the test is also very important. If a test fails it should be clear what is it that we are testing, under what circumstances and what the actual and the expected results were.
Knowing those things will help a lot while fixing the code.
Also, bear in mind that you should not only test the happy path, you should also ensure that the code under test throws the right exceptions when it has to or it performs the right validations.
Writing tests pay off. The time dedicated to writing tests is by far out-ranged by the time saved by detecting issues early. Testing also encourages good code architecture, resulting in less time spent in developing new features, bugfixing and spreading the knowledge of the application across the members of your team.