目录
介绍
背景
使用代码
兴趣点
简要说明了.NET Core数据库测试存在的问题。随后,通过GitHub上的具体代码示例说明了解决方案。
介绍对具有数据库依赖性的应用程序进行自动测试是一项艰巨的任务。因为数据库不是完全可模拟的,所以单元测试不会对您有所帮助。如果做一个update,delete或者insert,您可以在查询之后运行select查询来检查查询的结果,但是这样您就不会检查不需要的副作用。可能受影响的表比需要的多,或者执行的查询比需要的多。这是这些问题的解决方案。
背景拥有TDD for .NET Core的经验会有所帮助,最好具有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()
{
To do = 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 _entityChanges =
new List();
public TodoRepositoryTest(WebApplicationFactory webApplicationFactory) :
base(webApplicationFactory, @"Data Source=../../../../project3.db")
{
}
[Fact]
public async Task SaveItemTest()
{
// arrange
var todoItem = new TodoItem()
{
To do = "TestItem"
};
// act
var savedEntity = await _todoRepository.SaveItem(todoItem);
// assert
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;
}
}
该类具有以下方法和变量:
- _todoRepository:要测试的实例
- _entityChanges:要声明的entitychanges(更改的种类,例如添加/更新的内容以及实体本身)
- SaveItemTest:完成实际工作的测试方法。它创建方法参数,调用该方法,然后对所有相关的内容进行断言:如果为主键分配了一个值,如果实际上只有一个实体发生了更改,如果此实体所做的更改确实是增加(而不仅仅是更新)并且如果添加的实体具有我们期望的类型。我们对此断言,之后没有运行选择查询。这可能是因为在通过另一种方法运行测试时,我们仅接收到所有实体更改。
- AddEntityChange:这是刚才提到的另一种方法。它接收所有包含实体本身的实体更改。
- SetTestInstance要使用名为_todoRepository的测试实例,需要通过此方法进行设置。
从具有所有样板代码的基类中调用该SetTestInstance方法以设置数据库集成测试。这是基类:
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中,测试的初始化通常在构造函数中完成。一旦正确完成,就可以轻松地进行测试。这些是在那里最重要的方法:
- AddInMemoryCollection:在这里,我们设置特定于测试的配置参数,在本例中为连接字符串。
- AddSingleton:测试本身被解析为单例,以便从数据库上下文中获取更新。
- ReplaceDbContext:现有数据库上下文需要替换为继承自它的数据库上下文,以扩展其功能并可能更新测试。
- CreateClient:用于触发Program类和Startup类中的代码的方法调用。
- GetService:需要使用此方法调用来解析从其调用测试方法的实例。这是可能的,因为会触发Program类和Startup类中的代码。
- SetTestInstance:需要通过调用此方法来设置从其调用测试方法的实例。
由于我们在此处(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 中复制到一个update操作中,该操作在更改真正保存到数据库之后被调用。这样,我们的测试类始终会收到已断言的已保存更改。测试问题现已解决。完整的代码在GiHub上。
我真的很喜欢我发现的这种工作方式。编写样板代码很烦人,但这是一项一次性的工作。对于使用Entity Framework Core 3.1的每个数据库测试,该代码均可重用。我可以测试所有需要测试的东西。完成的update,insert和delete,受影响的实体以及更改的实体总数也很有意义。在测试select之后,无需运行任何查询就可以完成所有操作。