Unit testing 10 best practices

  • Home
  • Blog
  • Unit testing 10 best practices
You Are Here:

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 Developer”, 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 behaviour 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 behaviour 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. But 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 test and integration test is, 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 behaviour or business logic of an application. 

 

10 best practices of unit testing

 

1. Proper test method name

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 of writing good test method names. One of them is given below-

        
[Test]
public void IsActive_WhenIdIsOne_ReturnsTrue()
{

}
            
        

Here, the method name is made readable with underscores, 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- 

        
[Test]
public void IsActive_WhenIdIsOne_ReturnsTrue()
{
    // Arrange
    User sut = new User() { Id = 1 }; 

    // Act
    bool result = detector.IsActive(sut.Id);

    // Assert
    Assert.IsTrue(result);
}
            
        

Here, in the “Arrange” phase we initialised 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 with 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.

        
[Test]
public void Approve_PassNullOrStringAsParameter_ThrowsArgumentExceptions()
{
    Assert.Throws<ArgumentException>(() => stringCalculator.Approve(null));

    Assert.Throws<ArgumentException>(() => stringCalculator.Approve("123"));
}
            
        

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 asserts 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 executing 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 few tests because these methods are used to declare data which 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-

        
[Test]
public void IsActive_WhenThereIsUser_ReturnsTrue()
{
    var user = CreateUser();
    bool result = detector.IsActive(user);
    Assert.IsTrue(result);
}

[Test]
public void IsInactive_WhenThereIsUser_ReturnsTrue()
{
    var user = CreateUser();
    bool result = detector.IsInactive(user);
    Assert.IsTrue(result);
}   

private User CreateUser()
{
    var user = new User() { Id = 1 };

    return user;
}
            
        


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-

        
[Test]

public void Add_WhenThereAreTwoValues_ReturnsResult()
{
    var math = new Math();
    int value1 = 1;
    int value2 = 2;
    int expectedValue = 3;
    int result = math.Add(value1, value2);

    Assert.Equal(expectedValue, result);
}
            
        

 

6. Use minimum input

User minimum input to verify the behaviour 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-

        
public bool IsActive(UserDto user)
{
   if(user.Id == 1)
   {
      return true;
   }
    return false;
}
            
        

This method only needs the id property of UserDto. So there is no need to declare other properties in the test method.

        
[Test]

public void IsActive_WhenIdIsOne_ReturnsTrue()
{
    UserDto user = new UserDto() { Id = 1 }; 
    bool result = detector.IsActive(user.Id);
    Assert.IsTrue(result);
}
            
        

 

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 a 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 to introduce bugs in the test suite. Take a look at the following example- 

        
[Test]
public void IsActive_WhenIdIsGiven_ReturnsBool()
{
    UserDto user = new UserDto() { Id = 1 }; 

	if (user.Id == 1)
	{
		bool result = detector.IsActive(user.Id);

		Assert.IsTrue(result);
	}
	else
	{
		bool result = detector.IsActive(user.Id);

		Assert.IsFalse(result);
	}
}
            
        

This test will work perfectly. But to make it more maintainable we have to write two tests as there are two cases-

        
[Test]
public void IsActive_WhenIdIsOne_ReturnsTrue()
{
    UserDto user = new UserDto() { Id = 1 }; 

    bool result = detector.IsActive(user.Id);

    Assert.IsTrue(result);
}   

[Test]
public void IsActive_WhenIdIsNotOne_ReturnsFalse()
{
    UserDto user = new UserDto() { Id = 2 }; 

    bool result = detector.IsActive(user.Id);

    Assert.IsFalse(result);
}
            
        

 

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.

Popular Posts