在进行数据库操作测试时,经常会遇到一些挑战,因为数据库并不是完美的可模拟对象。当执行更新、删除或插入操作时,可以通过执行一个查询来检查结果,但这并不能检查到不期望的副作用。可能有更多的表受到了影响,或者执行了更多的查询。本文将介绍如何解决这类问题。
对于.NET Core的测试驱动开发(TDD)有一定的了解会很有帮助,最好有xUnit和EF Core的经验。
首先,来看要测试的代码。这里有一个数据库上下文依赖注入,以及一个将数据保存到数据库的方法。添加并保存的实体作为方法的输出返回。
public class TodoRepository : ITodoRepository
{
private readonly ProjectContext _projectContext;
public TodoRepository(ProjectContext projectContext)
{
_projectContext = projectContext;
}
public async Task SaveItem(TodoItem item)
{
var newItem = new Entities.TodoItem()
{
Todo = item.Todo
};
_projectContext.TodoItems.Add(newItem);
await _projectContext.SaveChangesAsync();
return newItem;
}
}
逻辑上,这个依赖需要正确解决。在Startup类中有一个方法用于此目的。上面描述的存储库类在这里被添加,就像它需要的数据库上下文以及依赖它的控制器一样。
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddDbContext(options =>
{
var connectionString = Configuration["ConnectionString"];
options.UseSqlite(connectionString,
sqlOptions =>
{
sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name);
});
});
services.AddTransient();
}
现在需要使用依赖进行测试。这样的测试应该看起来像这样:
public class TodoRepositoryTest : TestBase
{
private ITodoRepository _todoRepository;
private readonly List<(object Entity, EntityState EntityState)> _entityChanges = new List<(object Entity, EntityState entityState)>();
public TodoRepositoryTest(WebApplicationFactory webApplicationFactory) : base(webApplicationFactory, @"Data Source=../../../../project3.db")
{
}
[Fact]
public async Task SaveItemTest()
{
var todoItem = new TodoItem()
{
Todo = "TestItem"
};
var savedEntity = await _todoRepository.SaveItem(todoItem);
Assert.NotNull(savedEntity);
Assert.NotEqual(0, savedEntity.Id);
Assert.Equal(todoItem.Todo, savedEntity.Todo);
var onlyAddedItem = _entityChanges.Single();
Assert.Equal(EntityState.Added, onlyAddedItem.EntityState);
var addedEntity = (Database.Entities.TodoItem)onlyAddedItem.Entity;
Assert.Equal(addedEntity.Id, savedEntity.Id);
}
public override void AddEntityChange(object newEntity, EntityState entityState)
{
_entityChanges.Add((newEntity, entityState));
}
protected override void SetTestInstance(ITodoRepository testInstance)
{
_todoRepository = testInstance;
}
}
这个类有以下方法和变量:
基类是:
public abstract class TestBase : IDisposable, ITestContext, IClassFixture>
{
protected readonly HttpClient HttpClient;
protected TestBase(WebApplicationFactory webApplicationFactory, string newConnectionString)
{
HttpClient = webApplicationFactory.WithWebHostBuilder(whb =>
{
whb.ConfigureAppConfiguration((context, configbuilder) =>
{
configbuilder.AddInMemoryCollection(new Dictionary
{
{ "ConnectionString", newConnectionString }
});
});
whb.ConfigureTestServices(sc =>
{
sc.AddSingleton(this);
ReplaceDbContext(sc, newConnectionString);
var scope = sc.BuildServiceProvider().CreateScope();
var testInstance = scope.ServiceProvider.GetService();
SetTestInstance(testInstance);
});
}).CreateClient();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public abstract void AddEntityChange(object newEntity, EntityState entityState);
private void ReplaceDbContext(IServiceCollection serviceCollection, string newConnectionString)
{
var serviceDescriptor = serviceCollection.FirstOrDefault(descriptor => descriptor.ServiceType == typeof(ProjectContext));
serviceCollection.Remove(serviceDescriptor);
serviceCollection.AddDbContext();
}
protected abstract void SetTestInstance(TTestType testInstance);
protected virtual void Dispose(bool disposing)
{
if (disposing) HttpClient.Dispose();
}
}
基类中最重要的部分是构造函数。在xUnit中,测试的初始化通常在构造函数中完成。一旦正确完成,测试就可以轻松实现。这些是构造函数中调用的最重要的方法:
由于在这里引入了一个新的依赖(TestProjectContext),需要实现这个依赖:
public class TestProjectContext : ProjectContext
{
private readonly ITestContext _testContext;
public TestProjectContext(DbContextOptions options, ITestContext testContext) : base(options)
{
_testContext = testContext;
}
public override async Task SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken())
{
Action updateEntityChanges = () => { };
var entries = ChangeTracker.Entries();
foreach (var entry in entries)
{
var state = entry.State;
updateEntityChanges += () => _testContext.AddEntityChange(entry.Entity, state);
}
var result = await base.SaveChangesAsync(cancellationToken);
updateEntityChanges();
return result;
}
}
每次保存实体变化(在本应用程序中总是通过SaveChangesAsync完成)时,变化从ChangeTracker复制到一个更新操作中,该操作在变化实际保存到数据库后被调用。通过这种方式,测试类总是接收到保存的变化以进行断言。现在测试问题已经解决了。完整的代码在GitHub上。