Mastering the subtle art of unit testing

Auteur
Alexander van Veelen
Categorieën
Publiceer datum

Accompanying your code with proper unit tests has numerous benefits. It provides protection against regression, provides documentation, and helps you to write better code.

If you want to take full advantage of these benefits you need to have high-quality unit tests. This article tries to provide rules and guidelines which can help you to write better unit tests and eventually, better software.

Easy to write. Easy to read. Easy to run.

Easy to write

When you find yourself in a situation where writing unit tests take too much effort or time, it could be an indication that your code is tightly coupled with other classes in your project.

Make sure you hide external dependencies behind an interface to make your unit test life easier. This way you can isolate the class you would like to test, without involving the rest of your codebase.

Sometimes you are stuck with some static classes of the framework you are using, like System.IO.File or System.DateTime. You can write abstractions for these classes yourself but sometimes someone did that for you and published these abstractions as a NuGet package, like System.IO.Abstractions.
Relying on those abstractions has the additional benefit of clearly specifying the boundaries of your application, and limiting boundary-crossing code to specific classes.

Easy to read

Naming your tests

The name of your test should express the intent of your test.

It should contain

  • the name of the method under test,
  • the scenario of the test and
  • the expected behaviour when the test is executed.

The name provides documentation of the behaviour of the method under test. If you come across some logic in a method which appears to have some weird behaviour at first glance, you can go to your unit tests and check if this is expected behaviour and refactor the code afterwards.

Bad

[Test]
public void Test_EmptyPhoneNumber() { … }

Better

[Test]
public void IsValid_GivenEmailAddressIsEmpty_ReturnsFalse() { … }

Structure your tests

Triple A testing is a common pattern used in unit testing. It consists of:

  • Arrange
    Setting up your objects
  • Act
    Making a call to the method under test
  • Assert
    Assert that the method is executed as expected

Following this pattern makes your code well-structured and easy to understand.

Bad

[Test]
public void IsValid_GivenEmailAddressIsEmpty_ReturnsFalse()
{
    Assert.IsFalse(new EmailValidator().IsValid(string.Empty));
}

Better

[Test]
public void IsValid_GivenEmailAddressIsEmpty_ReturnsFalse()
{
    // Arrange
    var emailAddress = string.Empty;
    var validator = new EmailValidator();

    // Act
    var result = validator.IsValid(emailAddress);

    // Assert
    Assert.IsFalse(result);
}       

The comments are not mandatory, but I just add them to make my tests extra clear.

Minimal passing tests

To verify the behaviour that you are testing, the input of your tests should be the simplest possible. Tests that include more information than necessary can make the intent of the test less clear.

The ObjectMother and builder patterns can help you to write tests which are smaller and easier to read.

Bad

[Test]
public void TrySetUserAsDriver_UserAgeIsUnder18_ReturnsFalse()
{
    // Arrange
    var car = new Car();
    var user = new User(
        "Alexander van Veelen", 
        new DateTime(2011, 1, 1), 
        new Address(
            "Stationsplein", 
            "45 Unit E4.194", 
            "Rotterdam", 
            "Netherlands"));

    // Act
    var result = car.TrySetUserAsDriver(user);

    // Assert
    Assert.IsFalse(result);
}

Better

[Test]
public void TrySetUserAsDriver_UserAgeIsUnder18_ReturnsFalse()
{
    // Arrange
    var car = ObjectMother.Car;
    var user = ObjectMother.UserBuilder
        .WithBirthDate(new DateTime(2011, 1, 1))
        .Build();

    // Act
    var result = car.TrySetUserAsDriver(user);

    // Assert
    Assert.IsFalse(result);
}

Avoid multiple asserts

A test should test one thing and should fail because of one reason. When using one assert per test you avoid testing multiple cases. In most testing frameworks it is not guaranteed that all the test cases are executed when one fails. It could be confusing when a test fails for multiple reasons.

If you would like to run the same test with different input variables you can use parametrized tests to avoid duplicating your tests.

Bad

[Test]
public void IsinValidator_WithInvalidValues_ReturnsFalse()
{
    var validator = new IsinValidator();

    var result = validator.Validate(string.Empty);
    Assert.IsFalse(result);

    var result2 = validator.Validate(null);
    Assert.IsFalse(result2);

    var result3 = validator.Validate(" ");
    Assert.IsFalse(result3);
}

Better

[TestCase("")]
[TestCase(null)]
[TestCase(" ")]
public void IsinValidator_InputIsEmpty_ReturnsFalse(string input)
{
    var validator = new IsinValidator();

    var result = validator.Validate(input);
    Assert.IsFalse(result);
}

Exception

There is one exception, though. If you want to test the setup of an object, it is ok to have an assert for each property:

[Test]
public void Constructor_WithParameters_SetsAllProperties()
{
    var name = "Alexander";
    var birthDate = new DateTime(1970, 1, 1);
    var address = new Address("stationsplein", "45", "Rotterdam", "Netherlands");

    var user = new User(name, birthDate, address);

    Assert.AreEqual(name, user.Name);
    Assert.AreEqual(birthDate, user.BirthDate);
    Assert.AreEqual(birthDate, user.Address);
}

NUnit has the Assert.Multiple method which checks all statements and accumulates all errors instead of terminating after one statement fails.
If you are using the Assertion framework “Shouldy” you can use ShouldSatisfyAllConditions.

Easy to run

Strong and independent tests

Unit tests should run isolated and should not affect, or depend on, the outcome of another test. You should not share an instance of an object between multiple tests.
NUnit has the [SetUp] attribute which you can attach to a method so it runs before every test. This way you can make sure that for every test run a new instance is created. I would recommend not to use this for the class under test but I use this to create a new mock of the dependencies of my class under test.

When you use xUnit, the constructor has the same behaviour as the [Setup] attribute of NUnit.

Bad

[TestFixture]
public class CarTests
{
    private readonly Car car;

    public CarTests()
    {
        this.car = new Car(new Engine());
    }

    [Test]
    public void Start_EngineIsOff_SetEngineToRunning()
    {
        // Arrange
        var myCar = this.car;

        // Act
        myCar.Start();

        // Assert
        Assert.IsTrue(myCar.Engine.IsRunning);
    }

    [Test]
    public void AnotherTest()
    {
        // Arrange
        var myCar = this.car;

        // Act
        myCar.Start();

        // Assert
        Assert.IsTrue(myCar.Engine.IsRunning);
    }
}

Better

[TestFixture]
public class CarTests
{
    [Test]
    public void Start_EngineIsOff_SetEngineToRunning()
    {
        // Arrange
        var myCar = CreateCar();

        // Act
        myCar.Start();

        // Assert
        Assert.IsTrue(myCar.Engine.IsRunning);
    }

    // other tests

    private static Car CreateCar()
    {
        return new Car(new Engine());
    }
}

Tests should be fast

It is not uncommon for a project to have hundreds or thousands of unit tests. The execution of a unit test should be completed in milliseconds, so you won’t be discouraged from running them after you changed some code.

The ObjectMother pattern

While writing unit tests you’ll notice that you often need to create some test objects to use in your tests.

If you want to test some discount calculator of a webshop, you’ll probably need to create some product, discount coupon and customer objects. Potentially this can be a lot of objects to create. You probably also need kind of the same objects in other tests. This can lead to unreadable tests and a lot of duplicated code.

You can move the creation of these objects to a special factory class: the ObjectMother.

In this ObjectMother you can create instances of objects with the properties set to a value which will be fine for most of your tests. If you need a slightly different version of the object for your test, you take the object from the object mother, change some properties and use this object in your test.

Example of a test using the ObjectMother pattern:

[Test]
public void CalculateDiscount_UserIsStudent_Returns50PercentDiscount()
{
    // Arrange
    var customer = ObjectMother.Customer;
    customer.EmploymentType = "Student";
    var discountCalculator = new DiscountCalculator();

    // Act
    var discount = discountCalculator.CalculateDiscount(customer);

    // Assert
    Assert.AreEqual(discount, 0.50m);
}

Using this pattern has numerous benefits:

  1. It simplifies and standardizes test object creation.
  2. Ease of maintenance, because test object creation is done in a specific class.
  3. Cleaner tests, the setup of your objects are moved to another file.
  4. It is easier to write new tests. You do not need to create all kinds of objects in your tests, just take them from the ObjectMother.

There is also a disadvantage of the ObjectMother pattern. Tests will depend on the exact data in the ObjectMother. Changing the default values may cause tests to break.

A good practice is to adjust the object, created by the ObjectMother, in such a way that it matches the test case. Even when the property is already set to the exact value as in the ObjectMother. This way the test will not break when the values changes in the ObjectMother.

In the example above the Employment Type may or may not be “Student” in the ObjectMother. When the value in the ObjectMother changes to “Self Employed” this test will not break.

The builder pattern

The object mother pattern works best when your objects are mutable; you can change the properties of your objects.

Robust applications often use immutable objects: you are unable to change the state of an instance of an object after initialization. This is a problem for the object mother because you can no longer reuse the same object and adjust the state of it for your test.

To address this problem, you can use the builder pattern. The builder will be a wrapper around the initialization of your object.

Example:

public static class ObjectMother
{
    public static CustomerBuilder Customer => 
        new CustomerBuilder("Alexander", "Programmer");
}

public class CustomerBuilder
{
    private string name;
    private string employmentType;

    public CustomerBuilder(string name, string employmentType)
    {
        this.name = name;
        this.employmentType = employmentType;
    }

    public CustomerBuilder WithName(string name)
    {
        this.name = name;
        return this;
    }

    public CustomerBuilder WithEmploymentType(string employmentType)
    {
        this.employmentType = employmentType;
        return this;
    }

    public Customer Build()
    {
        return new Customer(this.name, this.employmentType);
    }
}
[Test]
public void CalculateDiscount_UserIsStudent_Returns50PercentDiscount()
{
    // Arrange
    var customer = ObjectMother.Customer
        .WithEmploymentType("Student")
        .Build();

    var discountCalculator = new DiscountCalculator();

    // Act
    var discount = discountCalculator.CalculateDiscount(customer);

    // Assert
    Assert.AreEqual(discount, 0.50m);
}

Resources:

Terug naar boven

Wij waarderen en waarborgen je privacy. We willen tevens graag een zo goed mogelijke ervaring bieden op onze website. Daarom plaatsen we graag een aantal cookies op je computer om ons te helpen bij het personaliseren van de inhoud van onze website. Lees meer over het gebruik van cookies in het privacy statement.

Find out more about cookies or .