Cover Image for Writing Unit Tests for Services in C# using NUnit + Autofac
Abdulrahman Muhialdeen
Abdulrahman Muhialdeen

Writing Unit Tests for Services in C# using NUnit + Autofac

When it comes to software development, testing is an essential part of ensuring that the code we write is working as intended. Unit testing is one of the most popular ways to test code...

When it comes to software development, testing is an essential part of ensuring that the code we write is working as intended. Unit testing is one of the most popular ways to test code, and in this article, we’ll explore how to write unit tests for services in C# using the NUnit testing framework.

The Example Project

We’ll use an example project available on GitHub to demonstrate how to write unit tests for services. The project is a simple .NET Core API that manages orders. The API exposes endpoints to create, retrieve, update, and delete orders, and it uses a database to store the order data.

The GitHub repository for the project is available at TechnoRahmon/ServiceUnitTest.

Setting up the Project

Before we start writing unit tests, let’s set up the project. To run the project, you’ll need to have .NET Core installed on your machine.

NOTE: to skip the following steps, just open the solution project with VS

  1. Clone the GitHub repository by running the following command in your terminal or command prompt:
git clone https://github.com/TechnoRahmon/ServiceUnitTest.git
  1. Navigate to the project directory:
cd ServiceUnitTest

3.Run the project by executing the following command:

dotnet run

4.Once the project is running, open a web browser and go to http://localhost:5000/swagger/index.html to see the API documentation.

Writing Unit Tests

Now that we have the project set up, let’s start writing some unit tests. We’ll write tests for the OrderService class, which is responsible for managing orders.

Testing the CancelOrder Method

Let’s start by testing the CancelOrder method, which cancels an order. We'll write tests to cover the following scenarios:

  • Canceling an order that exists and is not already canceled or delivered.
  • Canceling an order that doesn’t exist.
  • Canceling an order that is already canceled.
  • Canceling an order that is already delivered.

Here’s the code for the CancelOrder method:

public async Task<(bool success, string message)> CancelOrder(string orderCode)
{
    var order = _dbService.FirstOrDefault<Order>(x => x.OrderCode == orderCode);
 
    if (order == null)
    {
        return (false, $"Order with id {orderCode} does not exist");
    }
    else if (order.Status == OrderStatus.Cancelled)
    {
        return (false, $"Order with id {orderCode} is already cancelled");
    }
    else if (order.Status == OrderStatus.Delivered)
    {
        return (false, $"Order with id {orderCode} is already delivered");
    }
    else
    {
        order.Status = OrderStatus.Cancelled;
 
        try
        {
            _dbService.Update(order);
        }
        catch (Exception ex)
        {
            return (false, $"Failed to cancel order with id {orderCode}");
        }
 
        return (true, $"Order with id {orderCode} has been cancelled");
    }
}

And here’s the code for the unit tests:

using Api.Models;
using Api.Services.DBService;
using Api.Services.OrderService;
using Moq;
using NUnit.Framework;
using static Api.Helper.Enums;
using System.Linq.Expressions;
using System.Threading.Tasks;
using System;
using Autofac.Extras.Moq;
 
namespace UnitTest
{
    public class Tests
    {
        [SetUp]
        public void Setup()
        {
        }
 
        #region Cancel Order test
        [Test]
        public async Task CancelOrder_OrderNotFound_ReturnsErrorMessage()
        {
            // Arrange
            string orderCode = "123";
 
            using (var mock = AutoMock.GetLoose())
            {
                // set up the expected value of the FirstOrDefault method in IDbService
                mock.Mock<IDbService>().Setup(x => x.FirstOrDefault<Order>(It.IsAny<Expression<Func<Order, bool>>>()))
                    .Returns((Order)null);
                
                // create a mock instance of IOrderService
                var orderService = mock.Create<OrderService>();
 
                // Act
                (var success , var message) = orderService.CancelOrder(orderCode);
 
                // Assert
                Assert.IsFalse(success);
                Assert.AreEqual($"Order with order code {orderCode} does not exist", message);
            }
        }
 
        [Test]
        public async Task CancelOrder_OrderAlreadyCancelled_ReturnsErrorMessage()
        {
            // Arrange
            string orderCode = "123";
 
            using (var mock = AutoMock.GetLoose())
            {
                var order = new Order { OrderCode = orderCode, Status = OrderStatus.Cancelled };
                mock.Mock<IDbService>().Setup(x => x.FirstOrDefault<Order>(It.IsAny<Expression<Func<Order, bool>>>()))
                    .Returns(order);
 
                var orderService = mock.Create<OrderService>();
 
                // Act
                (var success, var message) = orderService.CancelOrder(orderCode);
 
                // Assert
                Assert.IsFalse(success);
                Assert.AreEqual($"Order with order code {orderCode} is already cancelled", message);
            }
        }
 
        [Test]
        public async Task CancelOrder_OrderAlreadyDelivered_ReturnsErrorMessage()
        {
            // Arrange
            string orderCode = "123";
 
            using (var mock = AutoMock.GetLoose())
            {
                var order = new Order { OrderCode = orderCode, Status = OrderStatus.Delivered };
                mock.Mock<IDbService>().Setup(x => x.FirstOrDefault<Order>(It.IsAny<Expression<Func<Order, bool>>>()))
                    .Returns(order);
 
                var orderService = mock.Create<OrderService>();
 
                // Act
                (var success, var message) = orderService.CancelOrder(orderCode);
 
                // Assert
                Assert.IsFalse(success);
                Assert.AreEqual($"Order with order code {orderCode} is already delivered", message);
            }
        }
 
        [Test]
        public async Task CancelOrder_SuccessfulCancellation_ReturnsSuccessMessage()
        {
            // Arrange
            string orderCode = "123";
 
            using (var mock = AutoMock.GetLoose())
            {
                var order = new Order { OrderCode = orderCode, Status = OrderStatus.New };
                mock.Mock<IDbService>().Setup(x => x.FirstOrDefault<Order>(It.IsAny<Expression<Func<Order, bool>>>()))
                    .Returns(order);
 
                var orderService = mock.Create<OrderService>();
 
                // Act
                (var success, var message) = orderService.CancelOrder(orderCode);
 
                // Assert
                mock.Mock<IDbService>().Verify(x => x.Update(It.IsAny<Order>()),Times.Once);
                Assert.IsTrue(success);
                Assert.AreEqual($"Order with order code {orderCode} has been cancelled", message);
            }
        }
 
        [Test]
        public async Task CancelOrder_FailedToCancelOrder_ReturnsErrorMessage()
        {
            // Arrange
            string orderCode = "123";
 
            using (var mock = AutoMock.GetLoose())
            {
                var order = new Order { OrderCode = orderCode, Status = OrderStatus.InProgress };
                mock.Mock<IDbService>().Setup(x => x.FirstOrDefault<Order>(It.IsAny<Expression<Func<Order, bool>>>()))
                    .Returns(order);
                mock.Mock<IDbService>().Setup(x => x.Update(It.IsAny<Order>())).Throws(new Exception("Failed to cancel order"));
 
                var orderService = mock.Create<OrderService>();
 
                // Act
                (var success, var message) = orderService.CancelOrder(orderCode);
 
                // Assert
                Assert.IsFalse(success);
                Assert.AreEqual($"Failed to cancel order with order code {orderCode}", message);
            }
        }
    }
    #endregion
}

In order to test the behavior of the OrderService class in various scenarios, the tests utilize the AutoMock class from the Autofac.Extras.Moq namespace. This class creates an instance of the OrderService class with its dependencies automatically mocked, enabling the OrderService class to be isolated from its dependencies during testing.

The tests aim to cover all possible scenarios for the CancelOrder method. These scenarios include cases where the order cannot be found, where the order has already been canceled or delivered, and where the cancellation is successful. For each test, a mock of the IDbService interface is used to provide the expected behavior for the specific test scenario.


Conclusion

In conclusion, it is highly recommended for developers incorporate unit testing into their development process to ensure that their code is robust and error-free.

unit testing is an essential part of software development as it ensures that each component of the system is working as expected. In this article, we have explored how to write unit tests for a service layer using NUnit. We have seen how to create a mock of a database service and how to write tests for different scenarios.

By writing unit tests, developers can catch errors early in the development cycle, which leads to a reduction in the cost of fixing bugs. It also helps to ensure that the code is maintainable and can be easily extended in the future.

The sample project on GitHub demonstrates how to write unit tests for a service layer in a .NET Core application. With the code examples provided in this article and the sample project, developers can easily get started with writing their own unit tests for service layers.