A unit test is a small automated check. It checks a tiny bit of software. Unit tests can be written relatively easily and they run in a matter of milliseconds. They are commonly being triggered automatically when somebody submits code to a repository so that the quality of the software is being validated, automatically, before it’s deployed onto the production environment.
In reality, unit tests are often far from small. They’re often hard to maintain and it’s hard to understand what the code that’s being tested was supposed to do when a test fails. As a result, unit testing becomes a costly exercise that slows down the team and the test-results doesn’t really prove much.
The following guidelines describe how to write proper unit tests:
- Unit tests have one assert per test.
- Avoid if-statements in a unit test.
- Unit tests only “new()” the unit under test.
- Unit tests do not contain hard-coded values unless they have a specific meaning.
- Unit tests are stateless.
Guideline №1.) One assert per test
Unit tests are supposed to be small. In reality, they often aren’t. Many codebases contain tests with multiple asserts that require big setups as a result. That makes it hard to judge their validity, and it becomes unclear when to create a new unit test, or when to add to an existing one. As a result, this kind of tests grow and grow:
Avoid these! Incomplete, big test with many asserts that resulted in undescriptive names.
Test = documentation. A single assert per test guarantees that only one thing is being tested. Choosing a descriptive name that correlates with that assert results in a complete overview of what the unit does. That’s easy to extend and validate. As a bonus, the tests become living documentation, too!
Treat unit tests like production code. Keep it maintainable. Don’t repeat yourself. Use small, clear methods with clear names and single responsibility. Big methods are an anti-pattern. They are hard to understand, potentially buggy and they’re a potential maintenance issue. Write small unit tests with one assert per test:
Clear names with one assert per test.
Guideline №2.) Avoid if-statements in a test
An if-statement in a test means the test is designed to do different things, depending on the situation. If’s need testing. How will you test the test? And will you test the test that tests the test? Where does it end?
From a requirements perspective, something either works or doesn’t. That’s static. The outcome of a test becomes less trustworthy if the test is dynamic, and tests become dynamic when they do things differently, depending on a situation.
Guideline №3.) Unit tests only “new()” the unit under test.
Unit tests are small. The test scope of a unit test is limited to just one single method or class. From that perspective it would make sense to only use the “new ()” keyword to instantiate the class that contains the method you’re testing.
Combining different classes, running different input through them and asserting the outcome, makes the test less targeted. Which class needs fixing when the test fails? Example:
Avoid these! Which class needs fixing when the test fails? Calendar or Schedule?
Don’t instantiate the dependencies of the class you’re testing, using the “new ()” keyword. Use frameworks like Moq or NSubstitute to create mocks and stubs to replace them. That makes tests more targeted and they’ll provide better feedback, that way:
Only instantiate the unit under test (Calendar) in your unit test. The bug must be in the Calendar class.
Guideline №4.) Unit tests do not contain hard-coded values unless they have a specific meaning
Unit tests are supposed to be descriptive: Given some input, when the code is being executed, then the result must be something.
Don’t hard-code test-data. That makes it harder to understand the test. Assume the following test fails, would you know what the problem is? You’ll have to debug to figure out what’s wrong:
Avoid these: It’s hard to figure out what’s wrong when such a test fails.
When fixtures are being generated with a framework, the test becomes a lot clearer. When the following test fails, it indicates that some code regarding the validation of a phone number has changed. You won’t have to debug to figure it out:
Only define the properties that trigger the logic you’re testing. In this case,
it should be obvious that something regarding the validation of a
phone number has broken when this test fails.
Guideline №5.) Unit tests are stateless
Nothing is worse than passing tests, and a broken application in production. Stateful tests can cause false positives. Assume the following example:
Avoid these: Stateful unit tests make the outcome of the test debatable.
Assume the GivenAppointment_ThenErrorOccurs executes first, then the GivenAppointmentThenAppointmentAppearsInList test creates either a false positive or a false negative.
Design unit tests to be trustworthy. Design them to pass regardless of their execution order. The outcome of tests must be the same no matter how often they’re being executed.
Small, reliable, fast, targeted, trustworthy unit tests are a solid base of an effective test automation strategy. Unit-testing alone is not enough. They test only small bits of software in isolation. Because combining several units creates the business value, it’s crucial to test that, too. You’ll need different types of tests in different places, to ensure the quality of the software. Keep reading: Read how to create an effective test automation strategy.