ASP.Net Core Web API – Unit Testing With XUnit

Hello everyone, welcome back to .Net Core Central. Today I am going to start with Unit testing of the Time Management application, that I have started building from my blog post Creating First ASP.Net Core Web API Application. I will be using XUnit as the testing framework.

What is Unit Testing

In a nutshell Unit testing is testing of a single unit in isolation of everything else. Which means, while we do unit test, we do not test the dependencies for the unit. We usually mock the dependencies, and just test the logic of the unit. The unit test should be able to run in a Continuous Integration server, without needing any connection to another server or a network file.

What is XUnit

XUnit is an open source unit testing framework which supports both traditional .Net Framework as well as .Net Core Framework. For more information on XUnit head over to https://xunit.github.io/.

Creating XUnit Project

To start testing TimeManagement application, first I will have to create a new testing project. First of all I am going to open Visual Studio 2017, after that I am going to “TimeManagement” solution from the recent projects list. This will open up the “TimeManagement” solution. After the solution is opened, I am going to right click on the solution and select “Add” -> “New Project“. This will open up the New Project model window. In the New Project model window, I am going to select “.Net Core” -> “xUnit Test Project (.NET Core)” and give the name of the project “TimeManagement.UnitTest” and click “OK“.

Creating Business Layer

Before I can start testing, I want create a business layer. For that I will create a new class library by right clicking on the solution, select “Add” -> “New Project“. This will open up the New Project model window. In the New Project model window, I am going to select “.Net Core” -> “Class Library (.NET Core)” and give the name of the project TimeManagement.Booking and click “OK“. The TimeManagement.Booking library will be used for booking time functionality.

Booking Implementation

Inside the Booking class library, I will have an interface ITimeBookingProcessor and its implementation class TimeBookingProcessor. The ITimeBookingProcessor will have a single method BookTime. BookTime will take three parameters, the Employee instance, the date for which the time entry is done and the total duration. And it will have a return type of void. In the implementation of the TimeBookingProcess, I have focused on input validations alone for testing purposes. First I have validated the employee id, making sure it is not less than or equal to 0. After that I have validated the date passed, making sure it is not greater than today (that’s my enforced requirement). And lastly I have verified that the duration cannot be more than 9 hours.

public interface ITimeBookingProcessor
{
    void BookTime(Employee employee, DateTime date, decimal duration);
}

public class TimeBookingProcessor : ITimeBookingProcessor
{
    private readonly IBookingProcessor bookingProcessor;

    public TimeBookingProcessor(IBookingProcessor bookingProcessor)
    {
        this.bookingProcessor = bookingProcessor;
    }

    public void BookTime(Employee employee, DateTime date, decimal duration)
    {
        if(employee.Id <= 0) 
        { 
            throw new ArgumentOutOfRangeException("Employee ID cannot be less than 0"); 
        } 

        if(date.Date > DateTime.Today)
        {
            throw new ArgumentOutOfRangeException("Booking date cannot be greater than today");
        }

        if(duration > 9)
        {
            throw new ArgumentOutOfRangeException("You are working too hard, lets talk!");
        }
    }
}

Testing invalid conditions

First of all In the Unit Test project TimeManagement.UnitTest, I am going to rename the default Class1 to TimeBookingProcessorUnitTest. And than I will create a method named Test_Invalid_EmployeeId which will be used for testing invalid employee condition. When I pass an employee with an employee Id of 0 (which is the default integer value), it should throw an ArgumentOutOfRangeException. Similarly I will be testing all the argument exception cases. For the invalid date condition, I will pass a valid employee id, but date as tomorrow’s date. And similarly for the invalid duration case, I will be passing valid employee id and date, but invalid duration.

[Fact]
public void Test_Invalid_EmployeeId()
{
    var bookingProcessor = new Mock();
    var timeBookingProcessor = new TimeBookingProcessor(bookingProcessor.Object);

    Assert.Throws(() => timeBookingProcessor.BookTime(new Data.Employee(), DateTime.Today, 8));
}

[Fact]
public void Test_Invalid_Date()
{
    var bookingProcessor = new Mock();
    var timeBookingProcessor = new TimeBookingProcessor(bookingProcessor.Object);

    Assert.Throws(() => timeBookingProcessor.BookTime(new Data.Employee {Id=2 }, DateTime.Today.AddDays(1), 8));
}

[Fact]
public void Test_Invalid_Duration()
{
    var bookingProcessor = new Mock();
    var timeBookingProcessor = new TimeBookingProcessor(bookingProcessor.Object);

    Assert.Throws(() => timeBookingProcessor.BookTime(new Data.Employee { Id = 2 }, DateTime.Today, 10));
}

Also here the attribute [Fact] is used to indicate the XUnit framework that this function is the candidate of test. For running the test, I will use the “Test” -> “Run” -> “All Tests“. This will execute all the tests, and show in the test execution window.

Testing valid condition

Finally, we will be testing with valid arguments. Now here since the function BookTime returns void, it becomes very hard to test the output. For that I will go ahead and update BookTime to return a Boolean value. And here I will just return true. Since, if any of the validation fails, I will throw error. Otherwise the operation is successful.

public interface ITimeBookingProcessor
{
    bool BookTime(Employee employee, DateTime date, decimal duration);
}

public class TimeBookingProcessor : ITimeBookingProcessor
{
    private readonly IBookingProcessor bookingProcessor;

    public TimeBookingProcessor(IBookingProcessor bookingProcessor)
    {
        this.bookingProcessor = bookingProcessor;
    }

    public bool BookTime(Employee employee, DateTime date, decimal duration)
    {
        if(employee.Id <= 0) 
        { 
            throw new ArgumentOutOfRangeException("Employee ID cannot be less than 0"); 
        } 

        if(date.Date > DateTime.Today)
        {
            throw new ArgumentOutOfRangeException("Booking date cannot be greater than today");
        }

        if(duration > 9)
        {
            throw new ArgumentOutOfRangeException("You are working too hard, lets talk!");
        }

        return true;
    }
}

And than I will create a new test function for testing the valid condition and run the tests.

[Fact]
public void Test_Valid_Arguments()
{
    var bookingProcessor = new Mock();

    var timeBookingProcessor = new TimeBookingProcessor(bookingProcessor.Object);

    Assert.True(timeBookingProcessor.BookTime(new Data.Employee { Id = 2 }, DateTime.Today, 9));
}

 

Testing with database operations

Now I want to insert the booking time into database. For that I will create an interface IBookingProcessor in the TimeManagement.Data assembly. But I will not create any implementation for the interface. As I do not need that for unit testing. That is the beauty of using interfaces for contracts and inversion of control.

I will pass this interface to the constructor of the TimeBookingProcessor and call the function Create on the interface, without worrying about the actual implementation.

public class TimeBookingProcessor : ITimeBookingProcessor
{
    private readonly IBookingProcessor bookingProcessor;

    public TimeBookingProcessor(IBookingProcessor bookingProcessor)
    {
        this.bookingProcessor = bookingProcessor;
    }

    public bool BookTime(Employee employee, DateTime date, decimal duration)
    {
        if(employee.Id <= 0) 
        { 
            throw new ArgumentOutOfRangeException("Employee ID cannot be less than 0"); 
        } 

        if(date.Date > DateTime.Today)
        {
            throw new ArgumentOutOfRangeException("Booking date cannot be greater than today");
        }

        if(duration > 9)
        {
            throw new ArgumentOutOfRangeException("You are working too hard, lets talk!");
        }

        return bookingProcessor.Create(employee.Id, date, duration);
    }
}

Now that I have an implementation to call Create (which does not have any implementation), the BookTime will always return false in the context of XUnit testing, since it is the default value of a Boolean. So here, I have to use Moq framework to mock the call Create of the interface IBookingProcessor. And during mocking I will direct the framework to return true. And than finally I will run the test, which will execute successfully.

[Fact]
public void Test_Valid_Arguments()
{
    var bookingProcessor = new Mock();
    bookingProcessor.Setup(p => p.Create(It.IsAny(), It.IsAny(), It.IsAny())).Returns(true);

    var timeBookingProcessor = new TimeBookingProcessor(bookingProcessor.Object);

    Assert.True(timeBookingProcessor.BookTime(new Data.Employee { Id = 2 }, DateTime.Today, 9));
}

Here, I am using It.IsAny construct, so that I do not tie my mocking implementation to the possible combination of variables, that can be passed to the Create function.

Conclusion

Unit testing is extremely important for agile development. From my personal experience, it is much more easier to do changes in a rapid development environment, if you have better coverage with unit testing. I personally prefer to have good enough tests to give me confidence that I can make changes without worrying about defects. Unit testing adds cost to the overall projects, so you have to make the right balance between your test coverage vs rework cost.

I have a video with the steps followed for creating the application, here is the link to the YouTube video.

References:

Unit Testing Basics: https://msdn.microsoft.com/en-us/library/hh694602.aspx
XUnit: https://xunit.github.io/
Moq: https://github.com/Moq/moq4/wiki/Quickstart