Soon or later we all experienced the comfortable feeling of test green lights, assuring a non regression after a change on a critical component or right after a refactoring which impacted several internal interactions. It’s probably the main advantage of having a good test coverage over your project as part of a continuous integration build system: tests may not spot bugs immediately indeed, but they should in most of the cases quickly alert about a broken functionality and any important regression. Therefore, we want and we need to rely on unit tests, we want and we need to have proper and effective unit tests then.
Let’s hence try to list some important aspect of a good unit test which will definitely improve the quality of what is supposed then to prove the quality of your build (nope, we are not talking about any meta-quality though).
A unit test should be independent by any other test within the test suite, moreover each test method of a given unit test should be independent by any other method (they can be ran singly indeed). Isolation should be ensured at test level hence, but also at state level and ordering level. Tests should thus run in a sand-box (dedicated database if required, workspace, input set of data and so on).
If a test really needed to be executed in a certain order (rare case, probably to investigate further), you may think about the @FixMethodOrder annotation provided by JUnit 4.11 or, more advanced approach, to a @RunWith custom runner.
If a test couldn’t restore certain data, then make sure it doesn’t affect any other test (it shouldn’t if data were only related to the functionality under test, but it might happen if affected data were shared among the system).
We should always rely on tests, we should trust them for non regression and input coverage (expected input/output, empty input, boundary cases, out of range inputs and other type of invalid conditions). I personally reject the idea of “the shorter the better” related to unit tests length: indeed a unit test method should be short, but the unit test class may be pretty long and it doesn’t necessarily mean that it’s a bad coded test, because the concerned functionality under test may need to be tested using different type of inputs, scenarios, preconditions. In such a case, the longer the better, that is, we would be sure the test covered as much as possible of a certain component and hence reaching a good level of reliability.
Remember though to apply the SRP (Single Responsibility Principle) to test code as well, hence don’t mix up different features in the same test class (a test should be unique and focused), try instead to narrow a scenario per method, as long as it concerns the same functionality.
If you can’t test something, then consider refactoring, consider mocking, consider injection. Use mocking objects within a certain limit though, we want to rely on the existing tested components and not on ad-hoc objects, don’t make the test happy by mocking everything, defy it. Try to inject dependency via constructors of service implementations (check Google Guice approach, for instance). Avoid abuse of PowerMock usage (mock static classes, mock final classes), instead reconsider the applied design! Actually, when your choice is between two different designs, think about how would you test the concerned component: it might spot a weak point of one of the proposed solutions or at least it could help to better understand dependencies and coupling.
A good test should be easy to understand, maintain, change. It should actually be auto-documented by its names (class name, methods names), its workflow and assertions, applying the classic AAA pattern: Arrange (your input), Act (using functionality under test), Assert (results). That’s why you should always use setup and tear down methods for common setups of environment, data, fields, grouping common settings and leaving a more readable test method code.
Readability also helps to reduce redundancy in your code (DRY, Don’t Repeat Yourself, applies to test code as well), grouping required test support code to superclasses or helper classes (preferred approach). However, always group them by functionality, scope, domain, don’t end up with an endless utility class of static methods and don’t fall into the need of testing your testing support code, don’t add unrequested custom test frameworks (apply YAGNI at this level as well, You Ain’t Gonna Need It).
If your test is properly isolated, then it should already be pretty repeatable. Remember to always restore the system (or the sandbox used by your test) to its initial state, that is, leaving the sand-box as it was before launching the test (checking table counts, deleting any created entry even in case of test failure, asserting absence/presence of environment related elements, and so on). Don’t rely on hard-coded values, like identifiers or record keys, for instance, which would also limit the test to a certain behavior (predefined ordering of data, imposed matching, provided special cases), consider instead generating random or pseudo random values in these cases. Moreover, when using dates, don’t create future failures!
A test should run fast (in terms of milliseconds, at worst seconds), which means it should never get blocked on I/O operations, connections timeout, long running processing or at least is should never add much overhead on top of the invoked functionality. Keep in mind that a single test may also be reasonably slow, but it would then be added to an existing test suite already running hundreds of tests and you can’t wait hours before the end of your build. Hence, performance improvements apply to test code to a certain extent as well: in general, your test code shouldn’t be much different than your business code, you should thus apply the same guidelines and coding standards and never consider it as a “second-class” code.
If it is already repeatable and isolated (and fast), it can then be easily automated in most of the cases. You got it right then. Automation is not a direct property of a test, it is indeed a possible usage we make of it, the way we generally use it, and we can automate a certain test only if it was already well prepared, providing the aforementioned properties.
That’s it. Following these few and simple guidelines should help us to have more effective unit tests. After writing a new unit test, we should always double check their readability, wonder how much we would rely on them, how much they are accurate and isolated. No matter whether we are applying a Test Driven Development approach or not, our tests should provide these characteristics and our project would certainly gain enormous added values.