DEV Community

Vinícius Estevam
Vinícius Estevam

Posted on

Testing Services on .NET8 with NUnit and Moq

Introduction

Testing is a important part of building and maintainable applications, in this tutorial we will use some powerful tools that make writing and managing tests much easier. We will explore how to create and run unit tests in .NET 8 using NUnit and Moq for mocking dependencies.

Requirements

  • C#
  • .NET8
  • Docker
  • NUnit

What are we going to test? ?

A service responsible for creating orders in a pizza app, following the folder organization:

Create order interface

using PizzApp.Domain.Entities;

namespace PizzApp.Application.Features.OrderFeatures.Create
{
    public interface ICreateOrderService
    {
        Task<Result> Execute(CreateOrderRequest request, CancellationToken cancellation);
    }
}
Enter fullscreen mode Exit fullscreen mode

Create order service

using PizzApp.Domain.Entities;
using PizzApp.Domain.Interfaces;

namespace PizzApp.Application.Features.OrderFeatures.Create
{
    public class CreateOrderService : ICreateOrderService
    {
        protected readonly IOrderRepository _orderRepository;
        protected readonly ICustomerRepository _customerRepository;
        protected readonly IProductRepository _productRepository;
        protected readonly IUnitOfWork _unitOfWork;

        public CreateOrderService(
            ICustomerRepository customerRepository, 
            IProductRepository productRepository, 
            IOrderRepository orderRepository, 
            IUnitOfWork unitOfWork)
        {
            _customerRepository = customerRepository;
            _productRepository = productRepository;
            _orderRepository = orderRepository;
            _unitOfWork = unitOfWork;
        }

        public string CustomerErrorMessage = "Customer not registered in the system";
        public string InvalidItemQuantityError = "The item quantity must be greater than zero";
        public string OrderCreatedMessage = "Customer Order created with sucessful";

        public async Task<Result> Execute(CreateOrderRequest request, CancellationToken cancellationToken)
        {
            var result = new Result();

            bool hasItemWithInvalidQuantity = request.Items.Any(x => x.Quantity <= 0);

            if (hasItemWithInvalidQuantity)
            {
                result.Error(InvalidItemQuantityError);
                return result;
            }

            Customer? customer = await _customerRepository.GetCustomerById(request.CustomerId, cancellationToken);

            if (customer == null)
            {
                result.Error(CustomerErrorMessage);
                return result;
            }

            var order = new Order(request.CustomerId, new List<OrderItem>());
            result = await AddOrderItems(request, result, order, cancellationToken);

            if (result.IsSuccess)
            {
                await _orderRepository.Create(order);
                await _unitOfWork.Commit(cancellationToken);
                result.Success(OrderCreatedMessage, order.Id);
            }

            return result;
        }

        private async Task<Result> AddOrderItems(CreateOrderRequest request, Result result, Order order, CancellationToken cancellationToken)
        {
            foreach (var item in request.Items)
            {
                var product = await _productRepository.GetProductById(item.ProductId, cancellationToken);

                if (product == null)
                {
                    result.Error($"Item with ID {item.ProductId} does not exist on database");
                    return result;
                }

                order.AddItem(new OrderItem(product.Id, item.Quantity, product.Price));
            }

            return result;
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

In this tutorial, we will focus on one error case and one successful case, the other test cases will be implemented in the project and avaliable on github.

Within the same solution add a new NUnit test project, in this project we will also use the Moq librarie to mock the interfaces required by CreateOrderService.

Test Project Structure

1. Set Mock on interfaces

[TestFixture]
public class CreateOrderServiceTests
{
    protected Mock<IOrderRepository> _orderRepositoryMock = null!;
    protected Mock<ICustomerRepository> _customerRepositoryMock = null!;
    protected Mock<IProductRepository> _productRepositoryMock = null!;
    protected Mock<IUnitOfWork> _unitOfWorkMock = null!;
    protected CreateOrderService _service = null!;

    [SetUp]
    public void SetUp()
    {
        _orderRepositoryMock = new Mock<IOrderRepository>();
        _customerRepositoryMock = new Mock<ICustomerRepository>();
        _productRepositoryMock = new Mock<IProductRepository>();
        _unitOfWorkMock = new Mock<IUnitOfWork>();

        _service = new CreateOrderService(
            _customerRepositoryMock.Object, 
            _productRepositoryMock.Object, 
            _orderRepositoryMock.Object, 
            _unitOfWorkMock.Object);
    }
    // ...
}
Enter fullscreen mode Exit fullscreen mode

First Scenario: Create Order Should be Successful

2. Arrange the objects

[Test]
public async Task CreateOrder_ShouldReturn_Success()
{
    // Arrange
    var customer = new Customer("Walter White", "walterwhite@email.com", "99338844");

    var product1 = new Product("Margherita Pizza", "Classic pizza with tomatoes, mozzarella, and basil", 29.90m, ProductCategoryEnum.PIZZA);
    var product2 = new Product("Coca-Cola 350ml", "Refreshing soda can", 5.90m, ProductCategoryEnum.DRINK);
    var product3 = new Product("Tiramisu", "Classic Italian dessert with mascarpone and coffee", 17.90m, ProductCategoryEnum.DESSERT);

    var orderItems = new List<CreateOrderItemRequest>
    {
        new CreateOrderItemRequest(product1.Id, 1),
        new CreateOrderItemRequest(product2.Id, 2),
        new CreateOrderItemRequest(product3.Id, 2)
    };

    var command = new CreateOrderRequest(customer.Id, orderItems);
   // ...
}
Enter fullscreen mode Exit fullscreen mode

3. Setup the interfaces method

[Test]
public async Task CreateOrder_ShouldReturn_Success()
{
    // Arrange
    _customerRepositoryMock
        .Setup(x => x.GetCustomerById(customer.Id, new CancellationToken()))
        .ReturnsAsync(customer);

    _productRepositoryMock
        .Setup(x => x.GetProductById(orderItems[0].ProductId, new CancellationToken()))
        .ReturnsAsync(product1);

    _productRepositoryMock
        .Setup(x => x.GetProductById(orderItems[1].ProductId, new CancellationToken()))
        .ReturnsAsync(product2);

    _productRepositoryMock
        .Setup(x => x.GetProductById(orderItems[2].ProductId, new CancellationToken()))
        .ReturnsAsync(product3);
    // ...
}
Enter fullscreen mode Exit fullscreen mode
  • Setup the input/output method used by service on required interfaces.

4. Execute the service

[Test]
public async Task CreateOrder_ShouldReturn_Success()
{
    // Act
    var result = await _service.Execute(command, new CancellationToken());
}
Enter fullscreen mode Exit fullscreen mode

5. Check if result returned is what expected

[Test]
public async Task CreateOrder_ShouldReturn_Success()
{
    // Assert
    Assert.IsTrue(result.IsSuccess);
    Assert.That(_service.OrderCreatedMessage.Equals(result.Message));
    _customerRepositoryMock.Verify(x => x.GetCustomerById(It.IsAny<Guid>(), It.IsAny<CancellationToken>()), Times.Once);
    _productRepositoryMock.Verify(x => x.GetProductById(It.IsAny<Guid>(), It.IsAny<CancellationToken>()), Times.Exactly(3));
}
Enter fullscreen mode Exit fullscreen mode
  • Verify that the result is successful.
  • Ensure the returned message matches the expected value.
  • Confirm that the mocked interface methods are executed the expected number of times.

6. Running the test

Open the terminal an run the following code:

dotnet test
Enter fullscreen mode Exit fullscreen mode

If not working try to reference the soluction in the command:

dotnet test PizzApp.sln
Enter fullscreen mode Exit fullscreen mode

After tests running on can check in the results in console:

You can use the option provided by your IDE to execute the tests.

Second Scenario: Create Order Should return Invalid Item Quantity

In the CreateOrderService, the first parameter validation checks the order item quantity. If the value is less than or equal to zero an error message should be returned.

public async Task<Result> Execute(CreateOrderRequest request, CancellationToken cancellationToken)
{
    var result = new Result();

    bool hasItemWithInvalidQuantity = request.Items.Any(x => x.Quantity <= 0);

    if (hasItemWithInvalidQuantity)
    {
        result.Error(InvalidItemQuantityError);
        return result;
    }
Enter fullscreen mode Exit fullscreen mode

To test this scenario we will follow the same step of previous one: Arrange, Act and Assert. However this time we will only set up the resources that are necessary.

[Test]
[TestCase(0)]
[TestCase(-2)]
public async Task CreateOrder_ShouldReturn_InvalidItemQuantityError(int quantity)
{           
    var orderItems = new List<CreateOrderItemRequest>
    {
        new CreateOrderItemRequest(new Guid(), quantity)
    };

    var command = new CreateOrderRequest(new Guid(), orderItems);

    // Act
    var result = await _service.Execute(command, new CancellationToken());

    // Assert
    Assert.IsTrue(!result.IsSuccess);
    Assert.That(_service.InvalidItemQuantityError.Equals(result.Message));
    _customerRepositoryMock.Verify(x => x.GetCustomerById(It.IsAny<Guid>(), It.IsAny<CancellationToken>()), Times.Never);
    _productRepositoryMock.Verify(x => x.GetProductById(It.IsAny<Guid>(), It.IsAny<CancellationToken>()), Times.Never);
}
Enter fullscreen mode Exit fullscreen mode

The TestCase attribute is a parameterized way to cover multiple values in the same scenario. In this case we use zero and a value below zero to verify if our implementation behaves correctly.

You can check the result of the each test on the Test Explorer, based on parameter provided in the TestCase:

Links:

You can access the full project in the following link

Top comments (0)