Unit testing is a very important quality measure that supports the IT delivery objective to deliver quality at speed. (note: unit testing is also called component testing) The main goal of unit testing is to verify that the implementation does what it intends to do. It is about individually testing the smallest units of code, referred to as modules or components. This allows developers to isolate each component, and whenever they are not according to expectation, fix problems at an early stage of the development lifecycle. Developers aim to test each part of the software in individual components as early as possible in the development process. For example, you have a password field and the requirements are that it needs to be at least 8 characters long, must contain alphanumerical characters and at least one symbol. Good practice is to create test cases that meet these criteria and, more important, also test cases that do not meet these criteria. So, a password like “secret01” should fail and “Secret123!” should pass the test.
Writing unit tests basically is just like writing production code. Well-written tests are assets while badly written tests are a burden. Following unit testing principles helps in creating good unit tests that pay off more than they cost.
The aim of DevOps teams is to build quality in. Testing the code on desired functionality and quality while writing the code is therefore an effective and efficient way to verify that the implementation actually does what it is intended to do. Some benefits of unit testing are:
- It provides a fast feedback-loop for verifying code changes.
- It provides a safety net – we know that the code works.
- It reduces costs and technical debt (e.g. because it reduces rework).
- It can be applied as regression test.
- It forms the basis for test automation.
- It facilitates easy verification of changes during maintenance.
Unit tests are an effective way to find faults or detect regressions, but unit tests should not be the only testing that is done. Unit tests, by definition, examine each unit of code separately. Unit testing (as part of Test Driven Development) supports designing software components robustly. But when an application is run for real, all those units also have to work together, and the whole is more complex and subtle than the sum of its independently tested parts. So other varieties of testing must also be organized by the team. For example, refer to the testing pyramid for more information.
That sounds great, but still some people may wonder: Why do you actually want a secondary system to help design or verify your code? Doesn’t your source code itself express the design and behavior of your solution?
The benefit of unit testing is correlated with the non-obviousness of the code under test
If you have code of any normal length, already it is not obvious at a single glance – so working out its exact behavior would take time and careful thought. Next additional design - and verification assistance (e.g., through unit testing) is essential to be sure that all situations are handled correctly. For example, if you’re coding a system of business rules or parsing a complex hierarchical string format, there will be too many possible code paths to check at a glance. In scenarios like these, unit tests are extremely helpful and valuable.
Unit testing takes time, apply good practices to be efficient
Designing and executing unit tests of course will require effort and time. On the other hand, it also brings benefits which makes this investment worthwhile.
Here we give some examples of the efforts needed for designing and executing unit tests.
- The time needed for writing unit tests
- The time spent fixing and updating unit tests, either because you’ve deliberately refactored interfaces between code units or the responsibilities distributed among them, or because tests broke unexpectedly when you made other changes
Some people avoid improving and refactoring application code out of fear that it may break a lot of unit tests and hence incur extra work. This of course is reversing the idea of quality engineering, keeping unit tests in sync with the application code is part of the work and when applying test driven development, creating and improving unit tests is just part of the development process.
To write good unit tests apply the “FIRST-U” rules: Fast, Isolated/Independent, Repeatable, Self-validating, Timely and Understandable.
These rules are described below:
Unit tests should be fast otherwise they will slow down your development/deployment time and will take longer time to pass or fail. E.g. in TDD short iterations of “change or add code” and running unit tests are performed. If the tests aren’t fast enough, this methodology loses its power. Typically, on a sufficiently large system, there will be a few thousand unit tests. If you have 2000 unit tests and the average unit test takes 200 milliseconds to run (which is be considered fast), then it will take 6.5 minutes to run the complete suite. 6.5 minutes may not seem long but imagine you run them multiple times a day, it will use a significant amount of your time. And imagine when the count of these tests increases because new functionalities are added to the application, it will further increase the test execution time. Then the value of your suite of unit tests diminishes as their ability to provide continual, comprehensive, and fast feedback about the health of your system also diminishes.
Never ever write tests which depend on other test cases. No matter how carefully you design them, there will always be possibilities of false alarms. To make the situation worse, you may end up spending a lot of time figuring out which test in the chain has caused the failure.
You should be able to run any one test at any time, in any order. By making independent tests, it’s easy to keep your tests focused only on a small part of behavior. When this test fails, you know exactly what has gone wrong and where. No need to debug the code itself.
The Single Responsibility Principle (SRP) of SOLID Class-Design Principles says that classes should be small and single-purpose. This can (and should) be applied to your tests as well. If one of your test cases can break for more than one reason, consider splitting it into separate test cases.
A repeatable test is one that produces the same result each time you run it. To accomplish repeatable tests, you must isolate them from anything in the external environment, not under your direct control. In these cases use mock objects. They are intended for this very purpose.
On occasion, you’ll need to interact directly with an external environmental influence such as a database. You’ll want to set up a private sandbox to avoid conflicts with other developers whose tests concurrently alter the database. A good practice is to use in-memory databases.
Tests must be self-validating. This means each test must be able to determine if the actual output is according to the expected output. This determines if the test is passed or failed. There must be no manual interpretation of results. (Manually verifying the results of tests is a time-consuming process that can also introduce more risk) Make sure you don’t do anything silly, such as designing a test to require manual arrange steps before you can run it. You must automate any setup your test requires – even do not rely on the existence of a database and pre-cooked data.
Create an in-memory database, create a schema and insert dummy data and then test the code. This way, you can run this test many times without fearing any external factor which can affect test execution and its result.
Practically, you can write unit tests at any time. You can wait up to code is production-ready, but you’re better off focusing on writing unit tests in a timely fashion. Using Test Driven Development is a good practice to follow.
As a suggestion, you should have guidelines or strict rules around unit testing. You can use review processes or even automated tools to reject code without sufficient tests.
The more you unit test, the more you’ll find that it pays off to write smaller chunks of code before tackling a corresponding unit test. First, it’ll be easier to write the test, and second, the test will pay off immediately as you flesh out the rest of the behavior in the surrounding code.
This is probably the most important practice, even though often missed. A unit test should have a title in the form of a user story or a description of what it does and what to expect. Don’t just give a unit test a simple number such as test1, test2, test3, etc... but assign useful and meaningful names.
Anatomy of a unit test case
The typical anatomy of every unit test is arrange-act-assert or given-when-then. This pattern is a standard across the industry. In the arrange (given) section the unit which is tested is initialized in a specific state. Mocks are created, and the expected result is set. The act section is the actual execution of the unit test case. The assert section compares the actual output of the act section with the expected output set in the arrange section. Below is a snippet of Java unit test code which give an example of the anatomy of a unit test.
@DisplayName("Test – Allow access boatride TestLand")
// Arrange: Create a visitor, set length of visitor to 1.85 mtr
// Act: Actual invocation of LengthValidator, and testing the functionality
// Assert: Check output of Act stage with expected result
The cost of unit testing a certain code unit is very closely correlated with its number of dependencies on other code units.
Below the costs and benefits of unit testing are put in a simple diagram.
- Complex code with few dependencies (top left).
Typically, this means self-contained algorithms for business rules or for things like sorting or parsing data. This cost-benefit argument goes strongly in favor of unit testing this code, because it’s cheap to do and highly beneficial.
- Trivial code with many dependencies (bottom right).
This quadrant has been labelled “coordinators”, because these code units tend to glue together and orchestrate interactions between other code units. This cost-benefit argument is in favor of not unit testing this code: it’s expensive to do and yields little practical benefit. Your time is finite; spend it more effectively elsewhere.
- Complex code with many dependencies (top right).
This code is very expensive to write with unit tests, but too risky to write without. Usually you can sidestep this dilemma by decomposing the code into two parts: the complex logic (algorithm) and the bit that interacts with many dependencies (coordinator).
- Trival code with few dependencies (bottom left).
We needn’t worry about this code. In cost-benefit terms, creating unit tests is so easy that you should just do it.