A unit test tests a particular unit of code – its definition is self explanatory. A unit can be considered to be the smallest testable part of the application. In practice, it is usually one function or method. We are all familiar with them, know a few unit test frameworks and are aware that there are dozens of them in the market. What developers often forget is that unit tests do not prove that an application works as intended. They proves that an application’s particular pieces of code work as the developer expects.
Nevertheless, unit tests are an essential part of almost every project and their value is tremendous. The most common and obvious benefits include:
- very fast feedback – usually you are aware of any detected problems in minutes and can take care of them without leaving a coding flow. By the way, they are the most often executed tests.
- allows refactoring – with high unit test code coverage you can refactor without the fear of introducing regression. It is more or less easy (at least easier than without unit tests) to ensure that code provides the same functionality as before.
- enforces better design – writing proper unit tests forces you to fully separate each of the application layers (database access is the most obvious example), use dependency injection, and use many other good practices,
- provides some sort of living documentation – unit tests themselves define expectations regarding code behaviour. If a unit test is available for a piece of code you are analyzing, you should check it to know what is the expected output of particular inputs.
To fully benefit from unit tests several rules and good practices should be followed. There is not much gain if tests are slow, dependent on each other and hard to analyze. There are many books about the topic, analyzing and describing each practice in detail. Here is an excerpt of the most important ones which are a must have:
- do not involve UI – you should test a piece of code – this is not an integration or smoke test. However, in some rare cases UI might be involved if you are testing it separately to the whole application,
- use dependency injection – it is much easier to provide stub objects in comparison to standard class coupling,
- make sure that your tests are fast – it is assumed that you should be able to execute on average 100 unit tests per second. Actively monitor for long running test (even break a build) and refactor them,
- do not integrate with other systems – unit tests shouldn’t touch the database, file system, any messaging or event systems, etc. Those parts are reserved for integration and components tests.
- test one assumption per test – unit tests should check a particular piece of code for a particular behaviour. Do not test complicated flows (even in one class context) in one test. It is good practice to have only one assert statement per test,
- use testing framework – kind of an obvious one but still worth it to remember. If you need a custom framework, build it on a base of already available solutions. There is no point in reinventing the wheel,
- tests should be independent of each other – in other words, the result and end state of all previously executed tests should not affect the ones which will be executed. This is a problem of many projects where there was no emphasis on writing proper tests. As a result the effort related to maintaining and analyzing failures is much bigger than usual,
- use mock or stubs to test code behaviour in isolation – again, you should test only a particular piece of code. Changes in any other than the current one class should not break its unit tests.
This is ideal. However, violating any of the above principles usually leads to long executing and tightly coupled unit tests which are hard to maintain and analyze. As a result, developers lose their trust in them, they are executed less frequently and are not providing the value as expected. Such a situation is also referred to as having hybrid tests. They are supposed to be unit ones but some of them are touching the file system, some use in-memory database, some start part of application context. They are still quite fast, maybe they do not even depend on each other, but they are no longer unit tests. We should clearly distinguish them from unit tests and mark them as integration ones. However, it doesn’t prevent us from allowing developers to execute them easily during the build.
The above practices are the ones which I personally think are the most important. They are also the ones which are frequently not followed. Make sure that all developers within your project are aware of them. It is worth it to keep them in mind while writing new and refactoring old tests. What’s your experience with unit tests? Do you know any other rules and practices which are worth mentioning and almost always following?