具有数据库依赖性的.NET Core应用程序的集成测试

目录

介绍

背景

使用代码

兴趣点


简要说明了.NET Core数据库测试存在的问题。随后,通过GitHub上的具体代码示例说明了解决方案。

介绍

对具有数据库依赖性的应用程序进行自动测试是一项艰巨的任务。因为数据库不是完全可模拟的,所以单元测试不会对您有所帮助。如果做一个updatedelete或者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<Entities.TodoItem> 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<ProjectContext>(options =>
   {
       var connectionString = Configuration["ConnectionString"];
       options.UseSqlite(connectionString,
       sqlOptions =>
       {
            sqlOptions.MigrationsAssembly
               (typeof(Startup).GetTypeInfo().Assembly.GetName().Name);
       });
   });
   services.AddTransient<ITodoRepository, TodoRepository>();
}

可以解析要测试的依赖关系很好,但是现在我们需要将依赖关系用于测试目的。这样的测试应如下所示:

public class TodoRepositoryTest : TestBase<ITodoRepository>
{
    private ITodoRepository _todoRepository;

    private readonly List<(object Entity, EntityState EntityState)> _entityChanges =
            new List<(object Entity, EntityState entityState)>();

    public TodoRepositoryTest(WebApplicationFactory<Startup> 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<TTestType> : IDisposable, ITestContext, 
                IClassFixture<WebApplicationFactory<Startup>>
{
    protected readonly HttpClient HttpClient;

    protected TestBase(WebApplicationFactory<Startup> webApplicationFactory,
                       string newConnectionString)
    {
        HttpClient = webApplicationFactory.WithWebHostBuilder(whb =>
        {
            whb.ConfigureAppConfiguration((context, configbuilder) =>
            {
                configbuilder.AddInMemoryCollection(new Dictionary<string, string>
                {
                        {"ConnectionString", newConnectionString}
                });
            });
            whb.ConfigureTestServices(sc =>
            {
                sc.AddSingleton<ITestContext>(this);
                ReplaceDbContext(sc, newConnectionString);
                var scope = sc.BuildServiceProvider().CreateScope();
                var testInstance = scope.ServiceProvider.GetService<TTestType>();
                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<ProjectContext, TestProjectContext>();
     }

     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<ProjectContext> options, 
                             ITestContext testContext) : base(options)
   {
        _testContext = testContext;
   }

   public override async Task<int> 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的每个数据库测试,该代码均可重用。我可以测试所有需要测试的东西。完成的updateinsertdelete,受影响的实体以及更改的实体总数也很有意义。在测试select之后,无需运行任何查询就可以完成所有操作。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值