Background

This post comes about as we take a fresh look at unit testing at work and the tools that we use. We have been building out some cool stuff, but very much moving fast (and errm, sometimes breaking things), and have planned a bit of gardening and housekeeping for the next few sprints and then embedding unit testing further into our processes.

As a bit of an aside, technology and the rate of change is relentless and even when you’ve mastered something that, at the time was the best practice, it will inevitably get surpassed by something else. For some 15+ years I’ve been using nUnit (and jUnit before that having moving across from Java). It is a solid testing framework and served me well, but I’ve been meaning to explore xUnit for some time, and have finally carved some time out to have a play.

This post is the first of a few. This will give an intro to unit testing and will consolidate thoughts on the approach that we’ll be trying out next over at cortex towers.

What is unit testing?

Unit testing is the process of testing individual units or components of software in isolation. It involves testing each unit of code to ensure that it works as intended, and that it integrates correctly with other units. It is usually done by developers using automated testing tools and focuses on testing the smallest testable parts of the codebase.

Are there other types of testing?

  • Component Tests

Component testing is the process of testing a group of related units or components that perform a specific function within the software. It involves testing the interactions between components and ensuring that they work together correctly. Component testing is usually done by developers or testing teams using automated testing tools, and it can be done in isolation from the rest of the system.

  • End to End Tests

End-to-end testing is the process of testing the entire system from start to finish, including all the components and their interactions. It involves testing the software as a whole to ensure that it meets the requirements and works as intended. End-to-end testing is usually done by testing teams, and it can involve both manual and automated testing. It is usually the final step in the testing process before the software is released.

Why bother?

Unit testing is an indispensable part of software development and provide some direct and indirect benefits…

  • They validate individual components of software, ensuring each behaves correctly. By isolating parts, defects are easier to identify and rectify.
  • Unit tests document behavior, serving as a design guide and usage manual.
  • They tend to improve the structure of code (decoupling), making it more maintainable and less prone to bugs. It simplifies integration by ensuring units work separately before combining.
  • They safeguard against regression, alerting developers when a recent change breaks existing functionality.

Let’s look at a simple xUnit test

We’ll use the a string extension method Left() as our test subject. This method extracts the first maxChars characters from the input string. If the string is shorter than maxChars characters, it returns the entire string. maxChars is optional and by default it returns the first 10 characters.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
namespace MyUtils;

public static class StringExtensions
{
    public static string Left(this string sourceString, int maxChars = 10)
    {
        if (string.IsNullOrEmpty(sourceString))
        {
            return string.Empty;
        }

        return sourceString.Length > maxChars ? sourceString[..maxChars] : sourceString;
    }
}

So, for example, var leftie = "The cat sat on the mat.".Left(15); will return the value "The cat sat on ".

And a simple test for the case when called with an empty string is shown below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using FluentAssertions;

namespace MyUtils.Tests.Unit;

public class StringExtensionsTests
{
    [Fact]
    public void Left_ShouldReturnEmptyString_WhenSourceStringIsEmpty()
    {
        // Arrange
        var sourceString = string.Empty;

        // Act
        var result = sourceString.Left();

        // Assert
        result.Should().BeEmpty();
    }
    
    // ... more tests covering:
    //   - strings longer than the maxChars 
    //   - strings less than the maxChars 
    //   - changing the maxChars 
    //   - calling it upon a null string
}

Points to note

Unit Test Naming Convention

We will use the MethodName_ExpectedBehavior_StateUnderTest convention as the naming strategy as it provides clear, descriptive names for test methods.

  • MethodName: This part represents the name of the method that you’re testing.
  • ExpectedBehavior: This part describes what behavior is expected when the conditions of the test are met. It should be able to answer what outcome you expect from the method under certain conditions.
  • StateUnderTest: This part describes the state under which the method is tested. It generally includes the conditions under which the test takes place.

In our example, Left_ShouldReturnEmptyString_WhenSourceStringIsEmpty above.

In this example:

  • Left is the MethodName
  • ShouldReturnEmptyString is the ExpectedBehavior
  • WhenSourceStringIsEmpty is the StateUnderTest

The benefit of this convention is that the test’s purpose is immediately clear just from reading its name, making it easier to understand and maintain.

FluentAssertions

We will use the FluentAssertions nuget package. FluentAssertions is a .NET library that offers a more readable, fluent interface for writing assertions in your tests. Its powerful, syntax simplifies complex assertions on objects and data types, making tests easier to write, read, and maintain.

FluentAssertions are covered off in more detail here

The Trip-A approach

The AAA (Arrange-Act-Assert) approach is a common unit testing pattern that provides a clear structure for organizing test code. It involves three steps:

  • Arrange: In this step, you set up the necessary preconditions for the test, such as creating objects, initializing variables, and defining inputs.
  • Act: This step involves performing the action or operation that you want to test, such as calling a method or executing a function.
  • Assert: In this step, you verify that the result of the action is as expected by comparing it to the expected output or behavior. By following the AAA approach, you can create clear and readable tests that are easy to understand and maintain. It also helps you to identify the cause of any failures or errors that may occur during testing.

A deeper dive into xUnit

Lifecycle of an xUnit unit test

xUnit ensures that each test method runs in isolation, and any dependencies or shared resources are properly initialized and cleaned up. It does so by instantiating the test class afresh for each test and running the following in order…

  1. Test Class Initialisation: Before running any tests in a test class, the test framework will create an instance of the test class and call its constructor. If the class has a constructor that requires parameters, the framework will attempt to resolve those dependencies through a dependency injection mechanism. This is used in place of the [setup] attribute that nunit uses.

  2. Test Method Execution: Once the test method setup is complete, the test method itself is executed.

  3. Dispose: You can also use the IDisposable interface to release any unmanaged resources used by your tests, such as database connections or file streams.

You need to implement the IDisposable interface and include a Dispose method that releases any unmanaged resources. The Dispose method will be called by xUnit after all test methods have been executed, and it’s a good place to perform any cleanup.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class MyTest : IDisposable
{
    private readonly MyClass _sut;

    // setup
    public MyTest()
    {
        _sut = new MyClass();
    }

    [Fact]
    public void MethodName_ExpectedBehavior_StateUnderTest()
    {
        // Arrange

        // Act
        var result = _sut.MethodName();

        // Assert
        result.Should().BeEmpty();
    }

    // teardown
    public void Dispose()
    {
        // Dispose here
    }
}

In xunit, there are no dedicated setup and teardown methods like in nunit’s [setup] and [teardown] attributes or mstest’s [testinitialize] and [testcleanup] attributes. Instead, xunit encourages the use of constructor and idisposable for setup and teardown operations.

Xunit’s approach of using constructor and idisposable for setup and teardown operations aligns with its philosophy of simplicity, test isolation, flexibility, and interoperability, and provides a clean and efficient way to organize and execute unit tests.

Facts and Theories

In xUnit, Fact and Theory are two types of unit tests that you can use.

  1. Fact: A Fact in xUnit signifies a test method that should always succeed. It does not take any parameters, and no argument data can be passed to it. Fact is used when we have some deterministic piece of code that can have only one outcome — success.

  2. Theory: A Theory in xUnit signifies a test method that should succeed for certain input data. Theory is used when we have a method that should work for a range of data and can take parameters, which are defined using InlineData, ClassData, or MemberData attributes. It is basically a way of doing data-driven testing.

Let’s look at an example of a Theory test type.

We’ll use a DateTime extension method GetDaySuffix() as our test subject. This extension method returns the suffix for the day number of the month.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
namespace MyUtils;

public static class DateTimeExtensions
{
    public static string GetDaySuffix(this DateTime date)
    {
        string ordinal;

        switch (date.Day)
        {
            case 1:
            case 21:
            case 31:
                ordinal = "st";
                break;
            case 2:
            case 22:
                ordinal = "nd";
                break;
            case 3:
            case 23:
                ordinal = "rd";
                break;
            default:
                ordinal = "th";
                break;
        }

        return ordinal;
    }
}

So, for example, var suffix = DateTime.Parse("2021-01-01").GetDaySuffix(); will return the value "st".

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
using FluentAssertions;

namespace MyUtils.Tests.Unit;

public class DateTimeExtensionsTests
{
    [Theory]
    [InlineData(1, "st")]
    [InlineData(2, "nd")]
    [InlineData(3, "rd")]
    [InlineData(4, "th")]
    [InlineData(5, "th")]
    [InlineData(6, "th")]
    [InlineData(7, "th")]
    [InlineData(8, "th")]
    [InlineData(9, "th")]
    [InlineData(10, "th")]
    [InlineData(11, "th")]
    [InlineData(12, "th")]
    [InlineData(13, "th")]
    [InlineData(14, "th")]
    [InlineData(15, "th")]
    [InlineData(16, "th")]
    [InlineData(17, "th")]
    [InlineData(18, "th")]
    [InlineData(19, "th")]
    [InlineData(20, "th")]
    [InlineData(21, "st")]
    [InlineData(22, "nd")]
    [InlineData(23, "rd")]
    [InlineData(24, "th")]
    [InlineData(25, "th")]
    [InlineData(26, "th")]
    [InlineData(27, "th")]
    [InlineData(28, "th")]
    [InlineData(29, "th")]
    [InlineData(30, "th")]
    [InlineData(31, "st")]
    public void GetDaySuffix_WhenCalled_ReturnsExpected(int day, string expected)
    {
        // Arrange
        var date = new DateTime(2021, 1, day);

        // Act
        var result = date.GetDaySuffix();

        // Assert
        result.Should().Be(expected);
    }
}

System Under Test

“System Under Test” is normally abbreviated to sut. It’s a naming convention used to represent the main object or component that is being tested in a particular test case or suite.

Using _sut makes it clear what object is being tested and can make your tests more readable and maintainable.

The examples we have seen so far have been simple extension methods. Our tests will more likely be testing classes, be they utility, helper or service classes. You would typically declare you “System Under Test” at the top of your test suite (class).

Assume we have a Customer class with a FullName method that combines FirstName and LastName properties.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class Customer
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public string FullName()
    {
        return $"{FirstName} {LastName}";
    }
}

Then, a unit test class using xUnit.net might look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
namespace MyUtils.Tests.Unit;

public class CustomerTests
{
    // System Under Test
    private readonly Customer _sut = new()
    { 
        FirstName = "Tess", 
        LastName = "Tickles" 
    };

    [Fact]
    public void FullName_ReturnsCorrectFullName()
    {
        // Arrange - nothing further to arrange
        
        // Act
        var result = _sut.FullName();

        // Assert
        result.Should().Be("Tess Tickles");
    }
}

You would likely have many test methods testing the behaviour of the sut.

Wrapping up

This has introduced some of the xUnit basics, along with FluentAssertions. A key part of UnitTesting is mocking. We’ll look at this next.

Acknowledgements…

I usually use Udemy or youTube to rattle through a course, but decided to give Nick Chapsas’s course a try. His youTube material is excellent and are well worth subscribing to, but highly recommend this course. Nick’s videos are always excellent. He cuts out any unnecessary fluff and just tells you what you need to know.