Unit testing 10 best practices
It has been more than a decade that unit testing has become a mainstream practice in the software industry. But “mainstream” does not mean It has become universal. Plenty of software developers who claim themselves “Full Stack Developers”, still don’t know how to write testable code and unit tests, though they write good code.
We will talk about writing testable code in another blog. Today we are going to focus on the best practices of unit testing. Before knowing about the best practices we should know what unit test actually is and the difference between unit test and integration test.
What is Unit Testing?
Unit tests are methods that test the behavior of a small portion or unit of an application independently from other parts. It could be a service method, API controller, class etc. But It does not test the implementation detail or external dependency of the code. If the code is written with testability, unit tests are pretty easy to write. However, when you notice writing a unit test is taking a lot of time then you need to have a closer look at the tested method. It may need refactoring.
Difference between Unit Test and Integration Test
Unit tests verify the behavior of a relatively small part or unit of an application where integration tests are generated to check out different parts of an application working together in a real-life environment.
Let’s think about an example out of software. If we test a robot, then checking out just the hand or leg working separately is a unit test. However, testing whether the robot can walk in a particular environment is an integration test as the robot can not walk without the combined effort of different parts of the body.
One of the major differences between unit tests and integration tests is, that integration tests usually require external resources, like databases or web servers, to be present, but in unit tests, we replace the database or external dependency with a mock version so that we can test the behavior or business logic of an application.
10 best practices of unit testing
1. Proper test method name
The test method name should be readable. If you write a test method name like “Isactiveshouldreturnstrue” it becomes really tough to read. There are some conventions for writing good test method names. One of them is given below-
Here, the method name is made readable with underscores, and capital letters and it has been divided into three parts. The first part of the method “IsActive” is the name of the method which is being tested in this test method, “WhenIdIsOne” is the condition that is being applied in this test method, and “ReturnsTrue” is the expected result of the method we are testing here. A good test name makes it more maintainable.
2. Maintain three phases
A typical unit test consists of three phases. These three phases are called in an abbreviated form triple-A or AAA which simply means Arrange, Act and Assert. Take a look at the following code-
Here, in the “Arrange” phase we initialized a small part of the tested method which we call “sut” or “system under test”. Then we passed it in the tested method which is the “Act” part of our test and finally, we check whether the returned data of the tested method matches the expected result or not. This phase is called “Assert”. Some tests may not have the arrange phase, which is ok.
3. One assert per test method
One of the purposes of unit testing is to find out the bug in code without debugging it. If more than one assert is added in a method it may sometimes be difficult to find out exactly which assert failed.
Think of reading the output of this example test in the test runner. In this case, you will see only one failure in the runner. Therefore, you literally won’t have any idea which one has failed and you have to debug the whole method. This practice discounts the ability of unit testing.
It does not mean no test should ever contain more than one assertion. There might be cases where more assertions should take place. For example, when asserting against an object, it is generally acceptable to have multiple assertions against each property to ensure the object is in the state that you expect it to be in. Using fluent assertions makes the failure messages more readable in this case and allows execution of all other assertions even if any assertion fails. Moreover, the idea of using one assert per method is to focus on validating only what is needed for the use case you are testing and make assertions for those things.
4. Use helper methods to setup data
One unit test method should not be dependent on other unit test methods. If you require the same object or state for multiple tests, then use helper methods. “Setup method” in Nunit or “Constructor” in xUnit should not be used for common objects or states of a few tests because these methods are used to declare data that are the same for all the unit tests of a test suite.
“Setup method” forces you to use the same requirements for each test. But Each test will generally have different requirements to get the test up and running. In the following example, two unit tests are sharing the same object through a helper method-
5. Avoid magic strings
Unit tests should not contain magic strings. It makes unit tests less readable. Naming variables in unit tests is important. It prevents the need for the reader of the test to inspect the production code in order to figure out what makes the value special. In this case, a good approach is to assign the values to constants. Consider the following example-
6. Use minimum input
User minimum input to verify the behavior of a method. Tests that include more information than required to pass the test have a higher chance of introducing errors into the test and can make the intent of the test less clear. Consider the following method-
This method only needs the id property of UserDto. So there is no need to declare other properties in the test method.
7. Write tests earlier
Even if you don’t do TDD (Test Driven Development), you should write unit tests earlier in the SDLC. It’s better to write a few tests that cover basic functionality at the beginning. Add more tests over time when the development takes shape and more information is learned.
8. Test with different data
Don’t use the same data in every test. Use as much variation as you can. This makes it much easier to see immediately which test is failing and why.
9. Avoid conditional logic in tests
It is a bad practice to write conditional logic like if-else or switch in a test. It makes the test less readable and tough to maintain. It also increases the chance of introducing bugs in the test suite. Take a look at the following example-
This test will work perfectly. But to make it more maintainable we have to write two tests as there are two cases-
10. Write tests for bugs before fixing
When you find a bug, write a failing test before you fix the bug and make sure the test fails. After fixing the bug, run the test again. It should pass then.
Following the best practices makes unit tests more readable and saves a ton of time. It may seem annoying to use all of the best practices at the beginning, but when you get used to it, you will find the true benefit.