DB Integration tests in dotnet core applications
Create a new project
1
| dotnet new webapi -n Todoapi
|
Add EFCore and other required packages
1
2
3
| dotnet add package Microsoft.EntityFrameworkCore.InMemory
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
|
Your DBContext For Todo App
public class TodoContext : DbContext
{
public TodoContext(DbContextOptions<TodoContext> options)
: base(options)
{
}
public DbSet<TodoItem> TodoItems { get; set; }
}
Your Model
public class TodoItem
{
public long Id { get; set; }
public string Name { get; set; }
public bool IsComplete { get; set; }
public DateTime CreatedDateTime { get; set; }
public DateTime CompletionDateTime { get; set; }
}
Program.cs with Minimal Api Endpoints
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddAuthorization();
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
builder.Services.AddDbContext<TodoDbContext>(options =>
options.UseInMemoryDatabase("TodoList"));
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference();
}
app.UseHttpsRedirection();
app.UseAuthorization();
// Create TodoItem
app.MapPost("/todos", async ([FromBody] TodoItem todo, TodoDbContext context) =>
{
context.TodoItems.Add(todo);
todo.CreatedDateTime = DateTime.UtcNow;
todo.IsComplete = false;
await context.SaveChangesAsync();
return Results.Created($"/todos/{todo.Id}", todo);
}).Accepts<TodoItem>("application/json");
// Mark TodoItem as complete
app.MapPut("/todos/{id}/complete", async (long id, TodoDbContext context) =>
{
var todo = await context.TodoItems.FindAsync(id);
if (todo == null)
{
return Results.NotFound();
}
todo.IsComplete = true;
todo.CompletionDateTime = DateTime.UtcNow;
await context.SaveChangesAsync();
return Results.Ok(todo);
});
app.MapGet("/todos", async (HttpContext httpContext, TodoDbContext context) =>
{
var todos = await context.TodoItems.Where(x => !x.IsComplete).ToListAsync();
return todos;
})
.WithName("TodoList");
app.Run();
}
}
Create XUnit Test Project
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"/>
<PackageReference Include="xunit" Version="2.9.2"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2"/>
</ItemGroup>
<ItemGroup>
<Using Include="Xunit"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\TodoApi\TodoApi.csproj" />
</ItemGroup>
</Project>
|
Create a DBFixture Class
public class DBFixture : IDisposable
{
public DBFixture()
{
var options = new DbContextOptionsBuilder<YourDbContext>()
.UseInMemoryDatabase(databaseName: "TestDB")
.Options;
Context = new YourDbContext(options);
Context.Database.EnsureCreated();
}
public YourDbContext Context { get; }
public void Dispose()
{
Context.Database.EnsureDeleted();
Context.Dispose();
}
}
Create a Test Class
public class TodoItemTests(TodoContextFixture fixture) : IClassFixture<TodoContextFixture>
{
private readonly TodoDbContext _context = fixture.Context;
[Fact]
public async Task CreateTodoItem()
{
var todo = new TodoItem
{
Name = "Test Todo",
IsComplete = false,
CreatedDateTime = DateTime.UtcNow
};
_context.TodoItems.Add(todo);
await _context.SaveChangesAsync();
var todoFromDb = await _context.TodoItems.FindAsync(todo.Id);
Assert.Equal(todo.Name, todoFromDb?.Name);
}
[Fact]
public async Task MarkTodoItemAsComplete()
{
var todo = new TodoItem
{
Name = "Test Todo",
IsComplete = false,
CreatedDateTime = DateTime.UtcNow
};
_context.TodoItems.Add(todo);
await _context.SaveChangesAsync();
var todoFromDb = await _context.TodoItems.FindAsync(todo.Id);
Assert.False(todoFromDb?.IsComplete);
todoFromDb!.IsComplete = true;
todoFromDb.CompletionDateTime = DateTime.UtcNow;
await _context.SaveChangesAsync();
var updatedTodo = await _context.TodoItems.FindAsync(todo.Id);
Assert.True(updatedTodo?.IsComplete);
}
[Fact]
public async Task GetTodoItems()
{
var todos = new List<TodoItem>
{
new()
{
Name = "Test Todo 1",
IsComplete = false,
CreatedDateTime = DateTime.UtcNow
},
new()
{
Name = "Test Todo 2",
IsComplete = false,
CreatedDateTime = DateTime.UtcNow
}
};
await _context.TodoItems.AddRangeAsync(todos);
await _context.SaveChangesAsync();
var todoFromDb = await _context.TodoItems.ToListAsync();
Assert.Contains(todoFromDb, x => x.Id == todos[0].Id);
}
}
Run Tests
Points to remember
If you have multiple tables/repository classes and you are sharing data between tests, you should use locking mechanism to create DB only once and share the same instance across tests.
public class DBFixture : IDisposable
{
private static readonly object _lock = new();
private static bool _created;
public DBFixture()
{
lock (_lock)
{
if (!_created)
{
var options = new DbContextOptionsBuilder<YourDbContext>()
.UseInMemoryDatabase(databaseName: "TestDB")
.Options;
Context = new YourDbContext(options);
Context.Database.EnsureCreated();
_created = true;
}
}
}
public YourDbContext Context { get; }
public void Dispose()
{
Context.Database.EnsureDeleted();
Context.Dispose();
}
}