How to Interaction Tests with ASP.NET Core and xUnit

In the part two article, learn about writing Interaction unit tests aka Void Methods. We assert by verifying whether a particular method has been called or not.

So far in our journey of writing unit tests for void methods or command methods, which are meant to invoke some command onto the system, we have been looking at the different types of methods and how writing unit tests would differ for each method types – the Interation tests and the Computation tests.

For a Computation test we can simply assert the functionality by its return value and judge if the functionality has any issues.

But for Interaction tests where there are no return types, we assert the functionality of such methods by verifying whether a particular method has been called or not.

Let’s assume the ReaderController has method Read() which internally invokes the ReaderFactory for a Reader instance based on the tierId parameter value.

public class ReaderController : Controller 
{
    [Route("{tierId}")]
    public IEnumerable<Reader> Read(int tierId)
    {
        var reader = factory.Create(tierId);
        return reader.ReadContent();
    }
}

We have previously written unit tests on the ReaderFactory class to assert the return object type for a passed in tierId. In this let’s write unit tests on the method in ReaderController which has a dependency on the ReaderFactory via IReaderFactory interface injection.

Let’s create a new file ReaderController_UnitTests which holds the same.

We can test two functional flows:

  1. We can check for the return data when a tierId is passed to the Read() method
  2. We can verify if the Create() method on ReaderFactory class is called when we pass in any tierId.

The former test would be a query test and the later is a verification test and hence an interaction test.

Let’s add the package Moq to use in this project:

    > dotnet add package Moq

Let’s add test logic to the two test scenarios:

public class ReaderController_UnitTests
{
    [Fact]
    public void Read_TierOneReader_WhenCalled_ReturnsNonEmptyList()
    {
        //Arrange
        var factoryMoq = new Mock<IReaderFactory>();
        factoryMoq.Setup(x => x.Create(It.IsAny<int>()))
            .Returns(new TierOneReader());

        var controller = new ReaderController(factoryMoq.Object);

        //Act
        var data = controller.Read(1).ToList();

        //Assert
        Assert.True(data.Count() > 0);
    }

    [Fact]
    public void Read_NullReader_WhenCalled_InvokesCreate()
    {
    
        //Arrange
        var factoryMoq = new Mock<IReaderFactory>();
        factoryMoq.Setup(x => x.Create(It.IsAny<int>()))
        .Returns(new NullReader());
        
        var controller = new ReaderController(factoryMoq.Object);

        //Act
        var data = controller.Read(1).ToList();

        //Assert
        factoryMoq.Verify(x => x.Create(It.IsAny<int>()));
        factoryMoq.Verify(x => x.Print(It.IsAny<string>()), Times.AtLeastOnce);
    }
}

We have used a Mock object of ReaderFactory to write these tests; in the actual flow we would inject an instance of ReaderFactory to the ReaderController class through constructor injection and we would like to use a mock implementation of the IReaderFactory which is substituted by a real ReaderFactory instance during runtime.

The second method Read_NullReader_WhenCalled_InvokesCreate() is the actual interaction method, wherein we verify whether the call to Create() method is invoked or not. There’s another call to Print() method on the next line, which is actually called when there’s no valid Reader available for the tierId.

public class ReaderFactory : IReaderFactory
{
    public IReader Create(int tierId)
    {
        if (tierId == 1)
            return new TierOneReader();
        else if (tierId == 2)
            ...
        else
            Print("No Matching Readers Found");
        return new NullReader();
    }

    public void Print(string message)
    {
        Console.WriteLine(message);
    }
}

In this scenario we verify whether a call to Print() has been made, if the tierId has no valid if-else branch.

The Moq method factoryMoq.Verify(x => x.Print(It.IsAny()), Times.AtLeastOnce) takes an argument Times.AtLeastOnce which means that the call to the method Print() should happen atleast once during the invocation. Else the assertion would fail. Other flags available are Times.Ones, Times.AtmostOnce and such.

Running the Tests

Before test execution, we would need to add reference for the project ./Api/ReaderFactoryApp.Api.csproj within the project ./Api/ReaderFactoryApp.Tests.csproj

we can do it by the command,

    > dotnet add ./ReaderFactoryApp.Tests/ReaderFactoryApp.Tests.csproj  reference ./Api/ReaderFactoryApp.Api.csproj

This adds a reference to the API project in test project and should remove all the reference errors. We might also get an error when accessing the controller class within the test project, we can resolve it by adding a reference to the package Microsoft.AspNetCore.Mvc.Core

    > dotnet add package Microsoft.AspNetCore.Mvc.Core

This should solve all the build errors, and we’re good to go. Let’s build the projects by running the command against the solution:

    > dotnet clean && dotnet build

We can observe that the Editor shows options for Run Test on top of each method and each class. We can run the tests by either clicking on the link above the methods or by running a command:

    > dotnet test

Buy Me A Coffee

Found this article helpful? Please consider supporting!

And the result would be as below:

wp-content/uploads/2022/05/Capture2.png

In this way, we can write unit tests for command methods as well as query methods by using xUnit, Moq and dotnet CLI


Buy Me A Coffee

Found this article helpful? Please consider supporting!

Ram
Ram

I'm a full-stack developer and a software enthusiast who likes to play around with cloud and tech stack out of curiosity. You can connect with me on Medium, Twitter or LinkedIn.

Leave a Reply

Your email address will not be published. Required fields are marked *