JS SmallTips — unit tests: good practices
As a software developer, you probably know what unit tests are and why they are beneficial. However, sometimes, you might (just like me) struggle to fully grasp how to create effective and simple unit tests. Perhaps you find yourself reproducing someone else’s test code without a clear understanding of its purpose. So, instead of jumping right into the code, let’s take a step back and explore some tips that I hope will help us writing better unit tests.
First things first
When we say unit tests, we are referring to testing a unit-level piece of code (but still testable). A unit can be a function, a method, a class or even a React component. Since this type of testing focuses on the smallest parts of the application and serves as a base for other types of tests, unit tests are placed at the bottom of the testing pyramid (unit-tests; integration-tests; e2e-tests).
Why
While it’s quite obvious that testing your code helps ensure that the units work as intended and prevents bugs, there are additional advantages to unit testing. We will explore some of these benefits as we apply the following tips to our testing process
I. Be clear when structuring your tests
The suite of tests and each test case should be organized in a way that clearly defines what you are testing and what the expected results are. Testing frameworks like Jest provide us a way to structure tests into blocks, describe test suites, and specify test cases. When used effectively, this approach can enhance our understanding of both the tests and the code itself
As we can see, there’s no need to dig into the code ( the moduleA.isEvenNumber function) to understand what it does. The main advantage here is that unit tests can also work as code documentation, helping other developers better grasp the code implementation and even the business rules.
II. Mocking dependencies
What makes unit tests different from other types of tests (integration, e2e) is that it should only run against the unit you are testing, that means we must (while testing) isolate the code we are testing from its dependencies. For instance: Let’s say we want to test the moduleA.functionA and at some point in the code, this function calls another module’s function (moduleB.functionB). During testing, module.functionA should not rely on the actual implementation of its dependencies. That’s where mocks come in handy. Let’s see an example
Mock moduleA’s dependencies and force moduleB to respond as necessary
The benefits of mocking include the ability to control moduleB’s responses for each test case and to assert moduleA’s results to ensure it meets our expectations. Additionally, we isolated moduleA, so any changes in moduleB will not affect moduleA’s tests. Isolating the unit we are testing is quite important because if we don’t do this, we will end up with an integration-like-test. In the example above, moduleA depends on moduleB and moduleB can also have its own dependencies, so if we don’t mock it, we will have a dependency chaining effect, which can lead to really complex and slow unit tests.
II. Make it simple
Not only should the structure of your test be clear, but the implementation should also be simple. In fact, the test implementation should be simpler than the code we are testing. I have a “rule of thumb” that I use when writing unit tests: “if my unit test is becoming too complex, it might be time for some code refactoring.”
I mentioned earlier that unit testing has some not-so-obvious benefits, one of them being that unit tests can actually improve your code. If you’ve read books like “Clean Code”, you probably already know some tips for making your code cleaner, easier to understand, and more maintainable. Well, unit testing can help with this and can also encourage us to adopt good development practices like Test-Driven Development (TDD). One approach I find useful to simplify my tests is to follow a basic test case structure. You see, a test case has a kind of “basic anatomy”.
1º. given — prepare the test-case scenario
2º. when — execute the unit
3º. then — check the expected behavior or outcome of the test
Let’s see the previous example with some comments to help us to get the idea.
To close this section, here are some “red-flags”, you should consider, that indicates that your unit test is becoming too complex and/or your code need some refactoring.
- the unit you are testing is doing more than it should?
- can the unit be broken down into smaller parts, so you can test them separately?
- the 1º step (given) is getting too cumbersome? If so, this could mean that the unit you are testing rely on too many dependencies. Breaking down the unit into small pieces will help.
III. Test the behavior not only the results
When unit-testing, we tend to focus on the results. We wanna write a test where the unit’s results match our expectations. But, as we saw in the previous examples, sometimes the unit we are testing rely on other units, modules or a third-party code. We also saw that in cases like that a good practice to apply is to mock its dependencies. So, let’s say we are testing a moduleA.functionA and that function calls, conditionally, the moduleC.functionC, since the moduleA may or may not call moduleC, we might want to test the moduleA.functionA’s behavior and assert whether it is calling moduleC only when & how it should. Let’s see an example
Given some conditions, the moduleA.getNumberDescription may or may not call moduleC.describeNumberType
So let’s see how to test the moduleA.getNumberDescription function’s behavior
As we can see, asserting the behavior is as relevant as asserting the results. Sometimes more.
IV. Force failures
Sometimes our test code isn’t that simple, and its logic can be misleading. So here’s an extra tip: every time you run your tests and they pass..
..intentionally introduce some failures to ensure that your test doesn’t provide a false positive result and it’s passing and failing only when it should
So, that’s it. These were some of my basic tips to assist you in writing better unit tests. I hope you found them helpful, and stay tuned for a potential PART II with some more tips. Thank you for reading this far.