Testing Services
Testing services is a important part of .NET devolopment as it helps to ensure that the services are working as expected and also helps to catch bugs early in the development process. It's most likely for a service test to be a unit test, and it's important to test the service in isolation. This means that the service should be tested without any dependencies on external services or databases.
For this reason it's important to use dependency injection to inject mock services into the service being tested. This allows the service to be tested in isolation and ensures that the test is repeatable and reliable. This will also require that interfaces are used for the services so that the mock services can be injected into the service being tested.
Example Application
In this example we will create a simple Web API to maintain a user list. To get started create a new Web API project titled UserApp
and enabled controller support.
User Model and DB Context
Create a new Data
folder and insert the following files:
namespace UserApp.Data
{
public class User
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
}
}
using Microsoft.EntityFrameworkCore;
namespace UserApp.Data
{
public class UserAppContext : DbContext
{
public UserAppContext(DbContextOptions<UserAppContext> options) : base(options) { }
public DbSet<User> Users { get; set; }
}
}
namespace UserApp.Models
{
public class UserViewModel(string email, string firstName, string lastName)
{
public string Email { get; set; } = email;
public string FirstName { get; set; } = firstName;
public string LastName { get; set; } = lastName;
}
}
As part of this ensure the following NuGet packages are installed:
- Microsoft.EntityFrameworkCore
Service Interface and Implementation
First create a entity not found exception:
namespace UserApp.Infrastructure.Exceptions
{
public class EntityNotFoundException(string message) : Exception(message) { }
}
Then we can create the service interface and implementation:
using UserApp.Data;
namespace UserApp.Services
{
public interface IUserService
{
public Task<IEnumerable<User>> GetUsersAsync();
public Task<User> GetUserAsync(int id);
public Task<User> AddUserAsync(string email, string firstName, string lastName);
public Task<User> UpdateUserAsync(int id, string email, string firstName, string lastName);
public Task<User> DeleteUserAsync(int id);
}
}
using Microsoft.EntityFrameworkCore;
using UserApp.Data;
using UserApp.Infrastructure.Exceptions;
namespace UserApp.Services
{
public class UserService(UserAppContext context) : IUserService
{
private readonly UserAppContext _context = context;
public async Task<User> AddUserAsync(string email, string firstName, string lastName)
{
User user = new()
{
Email = email,
FirstName = firstName,
LastName = lastName
};
await _context.Users.AddAsync(user);
await _context.SaveChangesAsync();
return user;
}
public async Task<User> DeleteUserAsync(int id)
{
User? user = await _context.Users.FindAsync(id) ?? throw new EntityNotFoundException("User not found");
_context.Users.Remove(user);
await _context.SaveChangesAsync();
return user;
}
public async Task<User> GetUserAsync(int id)
{
User user = await _context.Users.FindAsync(id) ?? throw new EntityNotFoundException("User not found");
return user;
}
public async Task<IEnumerable<User>> GetUsersAsync()
{
return await _context.Users.ToListAsync();
}
public async Task<User> UpdateUserAsync(int id, string email, string firstName, string lastName)
{
User user = await _context.Users.FindAsync(id) ?? throw new EntityNotFoundException("User not found");
user.Email = email;
user.FirstName = firstName;
user.LastName = lastName;
await _context.SaveChangesAsync();
return user;
}
}
}
Setup Dependency Injection
We now need to add our DI config to the Program.cs
file. Add the following to the service section, you will also need to install the following NuGet packages:
- Microsoft.EntityFrameworkCore.InMemory
builder.Services.AddDbContext<UserAppContext>(options =>
{
options.UseInMemoryDatabase("UserApp");
});
builder.Services.AddScoped<IUserService, UserService>();
Controller
Finally we can create a controller to interact with the service:
using Microsoft.AspNetCore.Mvc;
using UserApp.Data;
using UserApp.Infrastructure.Exceptions;
using UserApp.Models;
using UserApp.Services;
namespace UserApp.Controllers
{
[Route("api/[controller]")]
public class UserController(IUserService userService, ILogger<UserController> logger) : ControllerBase
{
private readonly ILogger<UserController> _logger = logger;
private readonly IUserService _userService = userService;
[HttpGet]
public async Task<IActionResult> GetUsersAsync()
{
try
{
return Ok(await _userService.GetUsersAsync());
}
catch (Exception exception)
{
_logger.LogError(exception, "An error occurred while getting the users");
return StatusCode(500);
}
}
[HttpGet("{id}")]
public async Task<IActionResult> GetUserAsync(int id)
{
try
{
return Ok(await _userService.GetUserAsync(id));
}
catch (EntityNotFoundException exception)
{
_logger.LogError(exception, "User not found");
return NotFound();
}
catch (Exception exception)
{
_logger.LogError(exception, "An error occurred while getting the user");
return StatusCode(500);
}
}
[HttpPost]
public async Task<IActionResult> AddUserAsync([FromBody] UserViewModel user)
{
try
{
User createdUser = await _userService.AddUserAsync(user.Email, user.FirstName, user.LastName);
return CreatedAtAction("GetUser", new { id = createdUser.Id }, createdUser);
}
catch (Exception exception)
{
_logger.LogError(exception, "An error occurred while adding the user");
return StatusCode(500);
}
}
[HttpPut("{id}")]
public async Task<IActionResult> UpdateUserAsync(int id, [FromBody] UserViewModel user)
{
try
{
return Ok(await _userService.UpdateUserAsync(id, user.Email, user.FirstName, user.LastName));
}
catch (EntityNotFoundException exception)
{
_logger.LogError(exception, "User not found");
return NotFound();
}
catch (Exception exception)
{
_logger.LogError(exception, "An error occurred while updating the user");
return StatusCode(500);
}
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteUserAsync(int id)
{
try
{
return Ok(await _userService.DeleteUserAsync(id));
}
catch (EntityNotFoundException exception)
{
_logger.LogError(exception, "User not found");
return NotFound();
}
catch (Exception exception)
{
_logger.LogError(exception, "An error occurred while deleting the user");
return StatusCode(500);
}
}
}
}
Service Testing
Now that we have our service and controller setup we can start testing the service. We will be using MSTest and Moq to test the service.
Create a new MSTest project titled UserApp.Service.Test
and then add a new file titled UserServiceTests.cs
. We will first get started with two tests, to ensure our GetUsersAsync
method is working as expected.
using Microsoft.EntityFrameworkCore;
using UserApp.Data;
using UserApp.Services;
namespace UserApp.Service.Test
{
[TestClass]
public class UserServiceTests
{
private UserAppContext _context = null!;
[TestInitialize]
public void Initialize()
{
DbContextOptions<UserAppContext> options = new DbContextOptionsBuilder<UserAppContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
_context = new UserAppContext(options);
}
[TestCleanup]
public void Cleanup()
{
_context.Dispose();
_context = null!;
}
[TestMethod]
public async Task GetUsersAsync_NoUsers_ReturnsNoUsers()
{
// Arrange
UserService service = new(_context);
// Act
IEnumerable<User> users = await service.GetUsersAsync();
// Assert
Assert.AreEqual(0, users.Count());
}
[TestMethod]
public async Task GetUsersAsync_OneUser_ReturnsOneUser()
{
// Arrange
_context.Users.Add(new User() { Email = "[email protected]", FirstName = "John", LastName = "Smith" });
_context.SaveChanges();
UserService service = new(_context);
// Act
IEnumerable<User> users = await service.GetUsersAsync();
// Assert
Assert.AreEqual(1, users.Count());
Assert.AreEqual("[email protected]", users.First().Email);
}
}
}
Run these two tests and confirm that they pass. This will confirm that the GetUsersAsync
method is working as expected. You can now continue to write tests for the other methods in the UserService
class.
// ...
[TestMethod]
public async Task GetUserAsync_ForExistingUser_ReturnsUser()
{
// Arrange
User user1 = new() { Email = "[email protected]", FirstName = "John", LastName = "Smith" };
User user2 = new() { Email = "[email protected]", FirstName = "Jane", LastName = "Doe" };
await _context.Users.AddRangeAsync(user1, user2);
await _context.SaveChangesAsync();
UserService service = new(_context);
// Act
User user = await service.GetUserAsync(1);
// Assert
Assert.AreEqual("[email protected]", user.Email);
}
[TestMethod]
public async Task GetUserAsync_ForNonExistingUserAndNoUsers_ThrowsEntityNotFoundException()
{
// Arrange
UserService service = new(_context);
// Act and Assert
await Assert.ThrowsExceptionAsync<EntityNotFoundException>(() => service.GetUserAsync(1));
}
[TestMethod]
public async Task GetUserAsync_ForNonExistingUserAndUsers_ThrowsEntityNotFoundException()
{
// Arrange
User user1 = new() { Email = "[email protected]", FirstName = "John", LastName = "Smith" };
User user2 = new() { Email = "[email protected]", FirstName = "Jane", LastName = "Doe" };
await _context.Users.AddRangeAsync(user1, user2);
await _context.SaveChangesAsync();
UserService service = new(_context);
// Act and Assert
await Assert.ThrowsExceptionAsync<EntityNotFoundException>(() => service.GetUserAsync(3));
}
[TestMethod]
public async Task AddUserAsync_AddsUser()
{
// Arrange
UserService service = new(_context);
// Act
int oldCount = _context.Users.Count();
User user = await service.AddUserAsync("[email protected]", "John", "Smith");
int newCount = _context.Users.Count();
// Assert
Assert.AreEqual(0, oldCount);
Assert.AreEqual(1, newCount);
Assert.AreEqual("[email protected]", _context.Users.First().Email);
}
[TestMethod]
public async Task UpdateUserAsync_CanUpdateExistingUser_UserUpdated()
{
// Arrange
User user1 = new() { Email = "[email protected]", FirstName = "John", LastName = "Smith" };
User user2 = new() { Email = "[email protected]", FirstName = "Jane", LastName = "Doe" };
await _context.Users.AddRangeAsync(user1, user2);
await _context.SaveChangesAsync();
UserService service = new(_context);
// Act
await service.UpdateUserAsync(1, "[email protected]", "JohnNew", "SmithNew");
// Assert
User updatedUser = (await _context.Users.FindAsync(1))!;
Assert.AreEqual("[email protected]", updatedUser.Email);
Assert.AreEqual("JohnNew", updatedUser.FirstName);
Assert.AreEqual("SmithNew", updatedUser.LastName);
}
public async Task UpdateUserAsync_ForNonExistingUser_ThrowsEntityNotFoundException()
{
// Arrange
User user1 = new() { Email = "[email protected]", FirstName = "John", LastName = "Smith" };
User user2 = new() { Email = "[email protected]", FirstName = "Jane", LastName = "Doe" };
await _context.Users.AddRangeAsync(user1, user2);
await _context.SaveChangesAsync();
UserService service = new(_context);
// Act and Assert
await Assert.ThrowsExceptionAsync<EntityNotFoundException>(() => service.UpdateUserAsync(3, "", "", ""));
}
[TestMethod]
public async Task DeleteUserAsync_ForExistingUser_RemovesUser()
{
// Arrange
User user1 = new() { Email = "[email protected]", FirstName = "John", LastName = "Smith" };
await _context.Users.AddAsync(user1);
await _context.SaveChangesAsync();
UserService service = new(_context);
// Act
int oldCount = _context.Users.Count();
User user = await service.DeleteUserAsync(1);
int newCount = _context.Users.Count();
// Assert
Assert.AreEqual(1, oldCount);
Assert.AreEqual(0, newCount);
Assert.AreEqual("[email protected]", user.Email);
}
[TestMethod]
public async Task DeleteUserAsync_ForNonExistingUser_ThrowsEntityNotFoundException()
{
// Arrange
UserService service = new(_context);
// Act and Assert
await Assert.ThrowsExceptionAsync<EntityNotFoundException>(() => service.DeleteUserAsync(1));
}
// ...
Conclusion
This this example we have created a simple Web API, and tested the service using MSTest. This is a good starting point for testing services in .NET and can be expanded upon to test more complex services.