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
- Clone the GitHub repository by running the following command in your terminal or command prompt:
git clone https://github.com/TechnoRahmon/ServiceUnitTest.git
- 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.