C# Introduction to Unit Testing

C# Introduction to Unit Testing

This is something that I’ve been avoiding, putting off, I know it makes a lot of sense to do so I figured if I document Unit Testing then I might help somebody else learn it. From what I’ve read Unit Tests should be written prior to writing the new classes that contain the methods that define the new functionality that’s being requested. You can normally determine what those classes and methods should be upfront based on the requirements. In my day to day I’m the one that defines the requirements, writes the work methods and develops the application. In a previous job I learned to do it in that sequence since handing a developer a fully defined set of requirements and a work method with mocked images of the UI “User Interface”, keep’s developers honest. In my experience developers can easily misinterpret a requirements document, but having the full workflow and mocked UI focuses the developer helps them understand the process and expedites the UI development because they can see what you want.

However, with my current position I have gotten into bad habits since I do everything and typically just dive into the code which makes pre-writing the Unit Tests a little harder for me anyway, so I typically end up writing them at the end. Which can be detrimental since you tent to write the tests to the working code and without realizing it, bias the testing.

Prerequisits

I create a new Console application in Visual Studio and then created a class that we can test against, the methods inside will be bear minimum to satisfy the unit tests.

public class UserService
{
    public User GetUserById(int userId)
    {
        return new User { Id = userId, Name = "John Doe" };
    }
}

public class User
{
    public int Id { get; set; } = 0;
    public string Name { get; set; } = string.Empty;
}

Unit Testing Premise

Fundamentally, were going to create a new class that contains void methods, each method tests an existing method within an existing class, so its a one to one match. Each method should have a name that’s defined in three parts;

  • Class Name_
  • Method Name_
  • What Your Testing For

For Example: NetworkingService_GetLastPingTime_ReturnDate

Below is an example of the Framework for a Test, we have a class that has the same name of the class were testing with the word “Test” appended. Within the class we have a void method that defines a Test to Get a User by Id, that should return a User.

Within the Method the Test is always defined in three steps;

  • Arrange – Define anything that’s required i.e. Class, Variables, Mocks.
  • Act – Call the Method being Tested.
  • Assert – Test the Result.
public class UserServiceTest
{
    public void UserService_GetUserById_ReturnUser()
    {
        // Arrange

        // Act

        // Assert
    }
}

So lets flesh out the test, within the Arrange we need to Define that things that are required; New Up the UserService class, Define the User Id to get. Within the Act we will call the method to be tested. Finally in the Assert, we will test the result(s).

public UserService_GetUserById_ReturnUser()
{
    // Arrange
    var userService = new UserService();
    var userId = 1;

    // Act
    var user = userService.GetUserById(userId);

    // Assert
    if (user == null || user.Id != userId)
    {
        Console.WriteLine("Test failed");
    }
    else
    {
        Console.WriteLine("Test passed");
    }
}

The down side of this is that we have to new up this class and call each test method, this is where the Test framework comes in.

Visual Studio Test Framework

First thing we need to do is to add a new xUnit project, we will give it the same name as our existing project but well append Test to the end.

New xUnit Project

Now that we have the new project we need to add an additional packages to it.

New xUnit Project

Install the latest Fluent Assertions package through the NuGet package manager, to the new xUnit Project. This package makes unit testing more naturally speaking.

NuGet Fluent Assertions

Now that we have everything setup we can change our unit test that we created, to take advantage of the inbuilt framework.

You can either drag and drop the test class we created previously from the original project to the xUnit project or recreate it. I dragged and dropped it into the new project and then fixed the namespace, so we now have this.

xUnit Test Setup

From the top Menu bar select Test -> Test Explorer, this will display the Test Explorer window.

Test Explorer

This is where we can see the Tests that have been created, and these tests can be executed from here by selecting the green play button. Once the tests have been evaluated we will see which test have passed and those that failed.

Test Explorer Window

So lets get back to our code, we need to decorate our method with the ‘[Fact]’ decorator, this tells the Framework that this is something to execute as part of the tests. Additionally we can now take advantage of the framework and use the Assert keyword. Notice here we have four Asserts;

  • Equal – Ensuring that the returned User Id matches the User Id provided to the Get User Method.
  • NotNull – Ensuring that the User Name is not Null.
  • NotEmpty – Ensuring that the User Name is not Empty.
  • NotEqual – Ensuring that the User Id is not Zero.
public class UserServiceTest
{
    [Fact]
    public void UserService_GetUserById_ReturnUser()
    {
        // Arrange
        var userService = new UserService();
        var userId = 1;

        // Act
        var user = userService.GetUserById(userId);

        // Assert
        Assert.Equal(userId, user.Id);
        Assert.NotNull(user.Name);
        Assert.NotEmpty(user.Name);
        Assert.NotEqual(0, userId);           
    }
}

We can now execute the tests, by pressing the Green Play Button.

Passed Tests

Fact vs Theory

We have already seen the Fact decorator, and in the Arrange we pre-defined the Users Id. What if we want to define multiple User Id’s? This is where the Theory decorator can be used.

When using the Theory decorator we have to also define additional InlineData decorators which define the values that we want to test. Then we have to define an integer property within the signature. Essentially we moved the UserId property from within the method to the signature.

public class UserServiceTest
{
    [Theory]
    [InlineData(1)]
    [InlineData(2)]
    [InlineData(3)]
    public void UserService_GetUserById_ReturnUser(int userId)
    {
        // Arrange
        var userService = new UserService();

        // Act
        var user = userService.GetUserById(userId);

        // Assert
        Assert.Equal(userId, user.Id);
        Assert.NotNull(user.Name);
        Assert.NotEmpty(user.Name);
        Assert.NotEqual(0, userId);           
    }
}

When we execute the Tests now we can see each of the Tests based on the Inline Data being passed in.

Theory Tests

Fluent Assertions

Let’s take a look at both Test Cases but this time we will add Assertion using Fluent Assertions. Within the Assert section we will now use the result from the Act section. The Fluent Assertions library provides the Should extension which contains methods such as NotBeNull, Be, NotBe, NotBeNullOrWhiteSpace, and NotBeNullOrEmpty. A full list of methods can be found here.

[Fact]
public void UserService_GetUserById_ReturnUser()
{
    // Arrange
    var userService = new UserService();
    int userId = 1;

    // Act
    var result = userService.GetUserById(userId);

    // Assert
    Assert.Equal(userId, result.Id);
    Assert.NotNull(result.Name);
    Assert.NotEmpty(result.Name);
    Assert.NotEqual(0, userId);

    result.Should().NotBeNull();
    result.Id.Should().Be(userId);
    result.Id.Should().NotBe(0);
    result.Name.Should().NotBeNullOrEmpty();
    result.Name.Should().NotBeNullOrWhiteSpace();  
}

This can obviously be extended to the Theory in the same way.

[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
public void UserService_GetUserById_ReturnUser2(int userId)
{
    // Arrange
    var userService = new UserService();

    // Act
    var result = userService.GetUserById(userId);
    
    // Assert
    Assert.Equal(userId, result.Id);
    Assert.NotNull(result.Name);
    Assert.NotEmpty(result.Name);
    Assert.NotEqual(0, userId);

    result.Should().NotBeNull();
    result.Id.Should().Be(userId);
    result.Name.Should().NotBeNullOrEmpty();
    result.Name.Should().NotBeNullOrWhiteSpace();
    result.Id.Should().NotBe(0);
}