具有數據庫依賴性的.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之後,無需運行任何查詢就可以完成所有操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章