Mastering 10 Unit Testing Best Practices: Your Ultimate Guide


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 must quickly produce the code.

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 must understand the best practices.

The top 10 unit testing best practices discussed 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 deterministic 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 ten best practices for unit testing to avoid common pitfalls in your testing process.

What is unit testing?

So, you want to know what unit testing is all about, huh?

Well, let me break it down for you in simple terms.

Unit testing is a type of software testing where you test individual units or components of your application separately from the rest of the system. Every test checks a small piece of your code in isolation to check if it performs as expected and delivers the desired outcomes.

unit testing

Some of the benefits of unit testing include improved code quality, increased productivity, reduced cost of maintenance, and faster time-to-market. But let’s be real. Unit testing can be kind of tedious and time-consuming. However, it’s worth the effort, and it’s better to catch bugs early on than wait until the product is already in the market.

The developers themselves usually do this type of testing before the application is released to the testing or quality assurance team.

I mean, nobody wants to release buggy software that takes forever to fix, right?

That’s why the unit testing process is crucial in software development. It helps identify defects and errors early on in the software development cycle, making fixing them easier and less costly.

So, how to make the best possible tests? Well, follow the next best practices.

1. Write simple tests

Unit testing is a great way to ensure your code works as expected. A common reason to do unit tests is to ensure the regression bugs are caught.

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 it.

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.

2. Write fast tests

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

How fast do they need to execute? Well, you want to be able to execute your suite of unit tests (let’s say thousands of unit tests) in several seconds.

The saying “fast test is a good test” seems to apply the most to unit tests in software development.

If a test runner takes too long to execute a test suite, 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. You can do that if you make the test fast as possible. Developers should always run the unit tests before merging any code to the main 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 several hours to know everything’s good, you will soon stop executing the tests that often.

And that can lead to trouble.

3. Test one scenario per test

one class one interface

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. 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.

Covering one scenario with a single unit test 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 must use the extra time to identify what’s wrong correctly.

One scenario per unit test also means 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 write test cases that check various properties of a single object. However, having multiple asserts checking the results of different performed operations within a single unit test is not ok.

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);
}

I’ve been in situations where test code is repeatedly rewritten as the production code changes.

This isn’t ideal because it can lead to an acceptance testing suite that never really gets better. Instead, it just keeps getting rewritten repeatedly. However, I’ve found that avoiding duplicating any logic from production code is always better. 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 deterministic test

Isolated unit tests are tests that provide code coverage without interacting with the outside environment. In other words, they don’t interact with the:

  • database
  • network
  • file system

Therefore, they run in memory and are deterministic. By deterministic test, I’m referring to a test that always produces the same result since it doesn’t depend on the outside world. No extra rows in the database that could affect the row count. No network failure. No missing file.

As a result, an isolated test provides more stability for the software and is faster to run. This test passes and will continue to pass until the code is incorrect. It is also easier to maintain, as it needs to be changed much less often than an integration test.

The best way to have isolated tests is by using test doubles. A test double is a simulated substitute for a real class. Typically it 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.

Or your own written fake. 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. And 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 change your production code, even a small refactoring, a unit test fails. That test failure happens because you change how the code is structured. In other words, you have changed the implementation detail, 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 implementation details in your tests. To avoid writing brittle unit tests, you must distinguish between implementation detail and behavioral detail and only test for the latter.

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 integration testing or system testing. An integration test is a test that checks that software modules or units of work correctly when they are combined. A system test goes a bit further and checks how the whole system works.

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