code-snippets/docs/dotnet/service-testing.md

489 lines
15 KiB
Markdown
Raw Permalink Normal View History

2024-09-05 13:54:08 +10:00
# 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:
::: code-group
```csharp [User.cs]
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;
}
}
```
:::
::: code-group
```csharp [UserAppContext.cs]
using Microsoft.EntityFrameworkCore;
namespace UserApp.Data
{
public class UserAppContext : DbContext
{
public UserAppContext(DbContextOptions<UserAppContext> options) : base(options) { }
public DbSet<User> Users { get; set; }
}
}
```
:::
::: code-group
```csharp [UserViewModel.cs]
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:
::: code-group
```csharp [EntityNotFoundException.cs]
namespace UserApp.Infrastructure.Exceptions
{
public class EntityNotFoundException(string message) : Exception(message) { }
}
```
:::
Then we can create the service interface and implementation:
::: code-group
```csharp [IUserService.cs]
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);
}
}
```
```csharp [UserService.cs]
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
::: code-group
```csharp [Program.cs]
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:
::: code-group
```csharp [UsersController.cs]
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.
::: code-group
```csharp [UserServiceTests.cs]
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 = "john@test.com", 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("john@test.com", 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.
:::
```csharp [UserServiceTests]
// ...
[TestMethod]
public async Task GetUserAsync_ForExistingUser_ReturnsUser()
{
// Arrange
User user1 = new() { Email = "john@test.com", FirstName = "John", LastName = "Smith" };
User user2 = new() { Email = "jane@test.com", 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("john@test.com", 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 = "john@test.com", FirstName = "John", LastName = "Smith" };
User user2 = new() { Email = "jane@test.com", 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("john@test.com", "John", "Smith");
int newCount = _context.Users.Count();
// Assert
Assert.AreEqual(0, oldCount);
Assert.AreEqual(1, newCount);
Assert.AreEqual("john@test.com", _context.Users.First().Email);
}
[TestMethod]
public async Task UpdateUserAsync_CanUpdateExistingUser_UserUpdated()
{
// Arrange
User user1 = new() { Email = "john@test.com", FirstName = "John", LastName = "Smith" };
User user2 = new() { Email = "jane@test.com", FirstName = "Jane", LastName = "Doe" };
await _context.Users.AddRangeAsync(user1, user2);
await _context.SaveChangesAsync();
UserService service = new(_context);
// Act
await service.UpdateUserAsync(1, "johnnew@test.com", "JohnNew", "SmithNew");
// Assert
User updatedUser = (await _context.Users.FindAsync(1))!;
Assert.AreEqual("johnnew@test.com", updatedUser.Email);
Assert.AreEqual("JohnNew", updatedUser.FirstName);
Assert.AreEqual("SmithNew", updatedUser.LastName);
}
public async Task UpdateUserAsync_ForNonExistingUser_ThrowsEntityNotFoundException()
{
// Arrange
User user1 = new() { Email = "john@test.com", FirstName = "John", LastName = "Smith" };
User user2 = new() { Email = "jane@test.com", 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 = "john@test.com", 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("john@test.com", 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.