Unit Testing, Clean code and more …(Part 2)
Unit Testing, Clean code and more (Part 1) spoke about what is clean code, its importance and how do tests, especially unit tests, ensure clean code. It also mentioned different types of tests: Sociable and Solitary tests. Let’s continue from here.
Solitary tests attempts to isolate the unit under test and concentrate on testing its features and masks the other collaborators by using test doubles like dummy, fake, stub, spies and mocks. Let’s look at the differences between these different test doubles.
- Dummy — Is just a placeholder, does nothing else
- Fake — Has an implementation but is not fit for production. E.g in-memory database
- Stubs — Provides canned results to calls made
- Spies — Stubs that record information
- Mocks — tests against expectations of set calls and throws exceptions if calls are not made or unexpected calls made
To mock or not?
Out of the different test doubles described above, mocks are one of the most common ones used by developers. But it is very important to identify when a mock is really needed. One way to decide is to know what exactly needs to be verified with this test. Is it state or behaviour?
State Verification: verifying whether the method/function worked correctly by asserting the state of the unit under test and its collaborators after the method was executed.
Behaviour Verification: verifying the whether the interaction between the unit under test and its collaborators happened correctly.
Martin Fowler describes the classical and mockist styles of TDD (test driven development). The classical style focuses on using real objects wherever possible except if there is an awkward or not-so-straight-forward interaction between collaborators. Whereas, a developer believing in mockist style would mock all collaborators. You can read about it in detail here.
But what should I use?
While following TDD it is very easy to fall into the trap of mocking a lot or more than necessary. This also comes from the notion that 100% code coverage is mandatory hence mock like crazy until each line of code is covered. Many developers believe that mocking is a code smell due to these reasons.
Having said that, I think that mocking is definitely helpful and even necessary in few cases. A few thumb rules can help us decide where the test needs mocks or not:
- Setting up the collaborator : If it is easy to set up the collaborator and collaborator itself does not have too many dependencies, do we need to mock it?
- Test setup: If the test setup is too complicated due to mocks maybe you need to relook at it.
- Test isolation: It is very important to make sure that each test case can be run in isolation along with mocking.
- Test granularity: The granularity of tests must be maintained even if its using mocking. Each test case should test a minimal and specific functionality.
- Coupling with implementation: If your test has mocks and is literally testing the implementation line by line, you are not doing it right.
- Design styles: Picking mocks or not also depends on particular design styles being followed as a convention. My suggestion always is that a single code base should use similar design style across to avoid inconsistency in tests. Of course, exceptions are allowed.
Unit Tests: Things to remember
Having spoken about why unit tests are needed and how they help in maintaining a clean and extensible code, across both blogs, I want to highlight a few important things to keep in mind.
- Keep check on test run times and overall build times
- Generate coverage reports
- If you are spending too much effort in doing test setup, re-look at your design
- Tests should not get coupled to implementation
- Avoid flaky tests. Eg. DateTime usage
- Unit tests ideally should not interact with any external resource. Eg. Try not to make database calls. Mock where needed.
- Do not make external service API calls
- Unit tests are documentation, they should be readable
Test Driven Development (TDD)
Since we have been talking mainly about unit tests, this blog would be incomplete without a section about TDD.
Why is TDD needed? How does it help? TDD essentially means writing tests first and then write code to fix the tests. It means that developers have to think about the public interface that other code in an application which needs to integrate with. This leads to better design and extensible and readable code.
Many other advantages of TDD include better test coverage and hence fewer production bugs, rapid feedback, clean code, clear scope, safe refactoring, fewer bugs, tests serve as documentation, good design etc.
Following are a few points specifically about how TDD aids good design:
- Test set up is difficult : This means that the interaction between the collaborators may need a relook.
- Too many assertions in a single test : This means that the unit is doing more than needed.
- Too many tests for a given unit : This again would mean that maybe a lot of logic lies in a single unit.
- Too many tests affected by small change in code : This indicates high coupling of a component with many others, which may not be desirable.
- Leaky abstractions : TDD helps to expose leaky abstractions
Having said that, TDD is not simple. It needs time, and a lot of discipline and practise.
These were some very basic thoughts and ideas about unit testing and clean code. Of course we can get into the detail of many topics here and learn more. Till then, happy testing !!