Post

Integration testing in dotnet core(C#) without mocking

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

1
dotnet test

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();
    }
}
This post is licensed under CC BY 4.0 by the author.