Unit Testing Best Practices: The 10 Commandments


Say what you will about the Ten Commandments, you must always come back to the pleasant fact that there are only ten of them.

– H. L. Mencken

These days, you live in a world where virtually every application is developed in a dynamically changing environment. This environment can consist of a rapidly changing customer base, a continuously changing business landscape, or an evolving operating environment.

Whether you are a front-end, back-end, or full-stack developer, you need to produce the code quickly. And that code needs to be correct.

One of the things that can make the task of being a developer so difficult is writing unit tests. At least, that’s how some developers see it. But, tests are important because they find problems in code. Without these, you have to debug your code to fix it and can never be sure if you have completely fixed the issues. Or even worse, without tests, there is no guarantee that the issue won’t occur again in the future.

To effectively unit test your code, you need to understand the best practices.

The top 10 unit testing best practices discussed in this article are:

  1. Write simple tests
  2. Write fast tests
  3. Test one scenario per test
  4. Name your tests properly
  5. Don’t duplicate production logic
  6. Write isolated tests
  7. Don’t overuse mocks and stubs
  8. Execute test suite often and automatically
  9. Avoid test interdependence
  10. Only test the public behavior

This article will teach you the most important best practices when it comes to writing unit tests so that you can avoid common pitfalls.

1. Write simple tests

Unit testing is a great way to ensure that your code is working as expected. Unit testing is the process of creating a simple test for every small chunk of code to ensure that it’s working the way it should. A common reason to do unit tests is to make sure that the regression bugs are caught.

unit testing

However, the tests you are writing should be simple. If the test is too complicated, it will be difficult to figure out what went wrong when your test fails. Furthermore, it will be tough to maintain such a test.

The good unit test follows the AAA structure:

  • Arrange – in this part of the unit test case, you set up the system under test and do other setup operations.
  • Act – in this part, you call the action you want to perform. This is usually a call to the method you are testing.
  • Assert – in this final part of the test, you check that the result of the performed operation is correct.
arrange-act-assert

Simple test cases are easier to write, maintain, and understand, and it is much easier to know what is being tested when you read the unit test. Additionally, when you need to refactor some of your code, your tests might break. But if the test is simple, then it’s not hard to change it.

2. Write fast tests

Unit test not only does have to be simple, but it also needs to execute quickly.

The saying “fast test is a good test” seems to apply the most to unit tests in software development. If a test suite takes too long to execute, the developers will only run the tests when necessary. Unfortunately, this means that many bugs might slip into the final code that unit tests would have prevented. In addition, developers might not know that the code is flawed if they don’t execute the tests often.

You want to be able to stop as many bugs from being injected into the code as possible. Having fast execution time is how you can do that. Developers should always run the unit tests before merging any code to the master branch.

Unit testing can help you catch a lot of bugs. However, when you are working with a large codebase, executing many slow tests can quickly become a nightmare. If you have to wait for several hours to know that everything’s good, you will soon stop executing the tests that often. And that can lead to trouble.

3. Test one scenario per test

10-commandments

While performing manual testing, you typically test a range of scenarios. For example, you test that the bug has been resolved and that the related features work as expected, usability, and other important aspects. While there are many types of testing and many other variables in software testing, one constant is that your unit test should only cover one scenario.

If you cover one scenario with a single unit test, that helps you isolate which part of the program is the issue once a test fails. On the other hand, if you run a single test that covers multiple scenarios and becomes a failure, you have to use the extra time to identify what’s wrong correctly.

One scenario per unit test also means that you should have only one assert per test. However, that doesn’t necessarily mean that you should have one assert all the time. It is ok to have multiple asserts that check various properties of a single object. However, it is not ok to have multiple asserts checking the results of different performed operations within a single unit test.

4. Name your test properly

You should have a proper unit testing method naming convention. A good unit tests method naming convention should be both specific and concise. You should try to use long and descriptive method names with as many words as possible. This will help other developers decipher the code better.

Let’s say you have the following method:

SanitizeFirstName(string name)

Obviously, just by looking at the method’s name, you could conclude that the method sanitizes the first name within the application. Also, it will be easier for someone else to understand that the method could is used to clean the first name. However, if cleaning the first name was not the intended use for the method, that name is totally misleading.

It’s the same with the unit testing methods. It’s important to have a good naming convention because it will help other developers decipher the code.

The following names would be bad names for the test case that checks the above method:

  • Test1
  • SanitizeFirstName_test
  • Check_SanitizeFirstName

There are different method naming conventions, but the one I use and recommend is the following:

UnitOfWork_ExpectedBehavior_ScenarioUnderTest

UnitOfWork represents the unit you are testing. But this can also be a method name. In the above case, that would be SanitizeFirstName.

ExpectedBehavior is the output you are expecting when you put your unit of work under test.

ScenarioUnderTest represents the conditions under which you are checking the unit of work.

So, if you want to write a test method that checks that method returns exception when you pass null to the method, the name would be:

SanitizeFirstName_throws_exception_when_null_is_passed_as_parameter

5. Don’t duplicate production logic in your test

I come across this problem fairly often. To test the production code, you use the same logic in your test to come up with the expected result. What do I mean by that? Let me show you one example.

Let’s look at the following method:

public class Calculator
{
    public double CalculateGrossAmount(double netAmount)
    {
        return netAmount * 1.25; // tax rate of 25%
    }
}

This method calculates the net amount based on the net amount. So far, so good. The simple test that covers this method is as follows:

[Fact]
public void CalculateGrossAmount_calculates_correct_gross()
{
    var calculator = new Calculator();

    var result = calculator.CalculateGrossAmount(100);

    Assert.Equal(125, result);
}

To test additional inputs, you might use parameterized test and slightly change the test.

[Theory]
[InlineData(100)]
[InlineData(200)]
[InlineData(300)]
public void CalculateGrossAmount_calculates_correct_gross_amount(double netAmount)
{
    var calculator = new Calculator();

    var result = calculator.CalculateGrossAmount(netAmount);
    var expectedGross = netAmount * 1.25;

    Assert.Equal(expectedGross, result);
}

No big deal, right? Well, actually, it is.

Now you have duplication in your code. And as you know, duplication is the root of all evil. So the better way is to use another test method parameter to pass the expected gross amount.

[Theory]
[InlineData(100, 125)]
[InlineData(200, 250)]
[InlineData(300, 375)]
public void CalculateGrossAmount_calculates_correct_gross_amount_for_net(double netAmount, double expectedGross)
{
    var calculator = new Calculator();

    var result = calculator.CalculateGrossAmount(netAmount);
    Assert.Equal(expectedGross, result);
}

It’s not always easy to decide whether or not the logic for a particular test should be duplicated from a production code. For example, I’ve been in situations where test code is being rewritten repeatedly as the production code changes.

This isn’t ideal because it can lead to an acceptance testing suite that never really gets better – it just keeps getting rewritten time and again. However, I’ve found that it is always better to avoid duplicating any logic from production code. It’s important to take a step back and think about the decision to duplicate code because it will always be easier, in the long run, to keep your test code separated from production code.

So please don’t keep that logic in your unit tests. Your code is ever-changing, and it’s hard to keep up with all requirements from users. If you have to change the same logic in two places, production code, and tests, you will soon delete those tests and stop writing the new tests.

And that’s not what you want.

6. Write isolated test

Isolated unit tests are a type of testing that provides coverage without testing the functionality of anything surrounding it.

As a result, isolated unit tests provide more stability for the software and are faster to run. They are also easier to maintain, as they need to be changed much less often than integration tests, as they deal with one single logic that does not depend on anything else.

The best way to have isolated tests is by using test doubles. A test double is a simulated substitute for a real class. Typically a test double will be used in unit testing to provide a fake version of a component to isolate its behavior. A test double can be a mock object or a stub object. You can find the when you use which in this guide.

7. Don’t overuse mocks and stubs

As I mentioned in the previous point, mocks and stubs are a type of test double that replace the dependencies needed for the code under test and are useful for testing in unit tests. They can help you mimic a real system and test your code’s logic in isolation.

They are a powerful tool to have in your testing arsenal. They can make your code more testable, which can help you find bugs before they get shipped. However, it is possible to overuse mocks and mock everything, which will hinder the clarity of your test suite.

Too many mocks can cause tests to become brittle and difficult to reason about.

What do I mean by brittle?

I mean that every time you make a change in your production code, even a small refactoring, a unit test fails. And most often, it fails because you change how the code is structured and not functionality. In other words, you have changed the implementation detail and not the observable behavior.

If that’s the case with your unit tests, then it’s possible that you are using mocks and stubs too much and testing implementations details in your tests. To avoid writing unit tests that are brittle, you need to distinguish between implementation detail and behavioral detail and only test for the latter. This means you should really test the public behavior of your code and not the inner workings.

Another way to avoid using too many mocks and stubs is to cover the part of your application that talks to the outside environment with the integration testing. An integration test is a test that checks that software modules or units of work correctly when they are combined.

Use mocks sparingly and only when necessary.

8. Execute tests often and automatically

Tests are a crucial part of the software development process. A good developer will always try to automate their tests to allow for quicker feedback and quick iterations.

One of the tenets of practicing continuous integration is to execute tests often and automatically. To have automated unit testing execution, that often involves setting up a CI server such as:

Running a suite of tests every time a commit is made makes it possible to determine whether the change improved or broke the code. The ability to execute tests often and automatically is crucial in the development and testing of software.

continuous integration
Continuous Integration in a nutshell

Automating the process of testing the quality of a product is the best option for business, as it minimizes human error and eliminates the need for countless hours of boring work.

If you work in a team, you need to choose the best automated testing tool available for your needs. You will have several options that depend on the target business goals and what you need – not to mention the budget.

9. Avoid test interdependence

Unit tests are developed to validate individual units of code. Therefore, you should avoid test interdependence in your unit tests.

The term ‘test dependency’ is often used to describe how one unit test is dependent on the outcome of another.

For example, let’s say we have the two tests:

public class UnitTest1
{
    private static int _sharedVariable;

    [Fact]
    public void TestMethod1()
    {
        int expected = 5;
        TestMethod2();
        Assert.Equal(expected, _sharedVariable);
    }

    [Fact]
    public void TestMethod2()
    {
        //other test code
        _sharedVariable = 5;
    }
}

In this case the TestMethod1 calls and depends on the success of the TestMethod2. This is problematic because the TestMethod1 can fail if there is an exception inside TestMethod2.

To avoid this problem, you should avoid test interdependence in your unit tests.

For example, if you’re writing a test for a class and you need a pre-existing condition to pass, that’s the wrong way to write unit tests. Instead, it’s better to write individual and independent test methods. Also, make sure to put related tests in a single test class.

10. Only test the public behavior

Testing only public behavior with the unit tests is a good idea. It’s also a practice that many developers overlook.

You absolutely don’t want to test private state and private behavior. Why? Because private behavior is kept private for a reason. And also, when you test private behavior, it’s much harder to change the code later without breaking some tests in the process.

Public behavior is what matters in unit tests. What is public behavior? Public behavior, in this case, is how the object behaves when you call its public methods. It’s how it behaves when you call the methods that are defined with an interface.

What public methods are available? What is their expected behavior? Do they perform the task they are supposed to do? All of these things are important to test.

Also, as a general rule that will save you a lot of headaches in the future when you have to refactor, don’t make all the methods public by default. Why? Well, find out in this article.

Conclusion

There’s no doubt that unit testing is a technique that can be useful in software development.

When done correctly, this will allow developers to detect issues early on in the software lifecycle and provide high confidence levels in the application. In this article, you have learned about the 10 commandments for unit testing to get the most out of your testing effort.

These guidelines are agnostic to the project type and programming language your code is written in. That means they can be applied to your project, so it’s wise to be familiar with them.

Recent Posts