by Patrick Kerrigan, . Tags: Testing Programming
Unit tests are a crucial part of the software development process. When used well they give you the confidence and sense of security needed to make changes without introducing bugs or regressions. When used badly they become a hindrance, slowing down development and encouraging bad practice. One common mistake is to write unit tests which reach too far and introduce fragility to your test suite.
Unit tests should have a single reason to change
When writing production code you stick to the single responsibility principle. The same applies when writing unit tests (and tends to be automatic if you practice Test Driven Development). A test should generally test one specific thing, and only that thing.
Why?
Tests should not only show you when something's wrong, but what is wrong. They should serve as a list of functional requirements, or documentation setting out how the component being tested should work, and, when run, which of the requirements are being met. If tests cover more than one intended behaviour then it becomes difficult to locate the source of a problem by looking at their results. It also means that when a behaviour changes intentionally you can end up with unexpected test failures in parts of your test suite which seem unrelated. This reduces the function of a test suite significantly and slows down development. A more serious consequence of a fragile test suite is developers becoming complacent when they see a test failure and simply updating the assertion so that it passes (or skipping/removing the test entirely).
One logical assertion
The easiest way to make sure your unit tests only cover one behaviour is to restrict them to making one logical assertion each. If you're testing a component which retrieves an object from storage and hydrates it, then don't test that it returns the expected object and that it calls the storage layer properly in the same test, split this up; One test for the interaction with the storage layer, and another test to see if the object returned matches what you expected. For this second test it's fine to make multiple assertions against the returned object's properties to see if they're correct, as this still forms one logical assertion: "is the object what we expected?".
Use test doubles
While it's important to test your system as a whole, unit tests should not depend on parts of the system they're not testing. A failure in Component A should not cause failures in the tests covering Component B or the ability for you to use your test suite to locate the source of problems is diminished. If you've written your components to depend on interfaces rather than concrete implementations then it's extremely simple to create test doubles implementing the interfaces of your dependencies so that you can simulate their behaviour and/or return values.
Choose the right test doubles
Test doubles are great for keeping your tests focused on a single component, but can introduce problems of their own. It may seem tempting to use true mocks for all test doubles, especially if you already have them sitting around from other tests. While this may cut down on the code you need to write, it introduces coupling between your test and areas of the system which you are not currently testing.
As an example, let's say you're testing Component A which depends on Component B and Component C. You'll need a test which tests the interaction with component B. Using a true mock of Component C in this test will couple it to Component C's behaviour, and whenever Component C changes you risk breaking this unrelated test. If however you use a stub in place of component C to provide controlled inputs when they're needed you're only depending on an interface of Component C, not its behaviour. Of course the interaction with Component C should also be tested in its own independent set of tests.
Integration testing
It's important to note that while removing unnecessary dependencies from your unit tests and keeping them focused on a single behaviour will bring many benefits, it's still necessary that the behaviour of your system is tested as a whole. You should still write integration and acceptance tests alongside your unit tests to make sure that the real system components interact with each other correctly, and not just their test doubles.