目錄
簡要說明了.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<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的每個數據庫測試,該代碼均可重用。我可以測試所有需要測試的東西。完成的update,insert和delete,受影響的實體以及更改的實體總數也很有意義。在測試select之後,無需運行任何查詢就可以完成所有操作。