XUnit 依賴注入
Intro
現在的開發中越來越看重依賴注入的思想,微軟的 Asp.Net Core 框架更是天然集成了依賴注入,那么在單元測試中如何使用依賴注入呢?
本文主要介紹如何通過 XUnit 來實現依賴注入, XUnit 主要借助 SharedContext 來共享一部分資源包括這些資源的創建以及釋放。
Scoped
針對 Scoped 的對象可以借助 XUnit 中的 IClassFixture 來實現
- 定義自己的 Fixture,需要初始化的資源在構造方法里初始化,如果需要在測試結束的時候釋放資源需要實現
IDisposable
接口 - 需要依賴注入的測試類實現接口
IClassFixture<Fixture>
- 在構造方法中注入實現的 Fixture 對象,並在構造方法中使用 Fixture 對象中暴露的公共成員
Singleton
針對 Singleton 的對象可以借助 XUnit 中的 ICollectionFixture 來實現
- 定義自己的
Fixture
,需要初始化的資源在構造方法里初始化,如果需要在測試結束的時候釋放資源需要實現IDisposable
接口 - 創建 CollectionDefinition,實現接口
ICollectionFixture<Fixture>
,並添加一個[CollectionDefinition("CollectionName")]
Attribute,CollectionName
需要在整個測試中唯一,不能出現重復的CollectionName
- 在需要注入的測試類中添加
[Collection("CollectionName")]
Attribute,然后在構造方法中注入對應的Fixture
Tips
- 如果有多個類需要依賴注入,可以通過一個基類來做,這樣就只需要一個基類上添加
[Collection("CollectionName")]
Attribute,其他類只需要集成這個基類就可以了
Samples
Scoped Sample
這里直接以 XUnit 的示例為例:
public class DatabaseFixture : IDisposable
{
public DatabaseFixture()
{
Db = new SqlConnection("MyConnectionString");
// ... initialize data in the test database ...
}
public void Dispose()
{
// ... clean up test data from the database ...
}
public SqlConnection Db { get; private set; }
}
public class MyDatabaseTests : IClassFixture<DatabaseFixture>
{
DatabaseFixture fixture;
public MyDatabaseTests(DatabaseFixture fixture)
{
this.fixture = fixture;
}
[Fact]
public async Task GetTest()
{
// ... write tests, using fixture.Db to get access to the SQL Server ...
// ... 在這里使用注入 的 DatabaseFixture
}
}
Singleton Sample
這里以一個對 asp.net core API 的測試為例
- 自定義 Fixture
/// <summary>
/// Shared Context https://xunit.github.io/docs/shared-context.html
/// </summary>
public class APITestFixture : IDisposable
{
private readonly IWebHost _server;
public IServiceProvider Services { get; }
public HttpClient Client { get; }
public APITestFixture()
{
var baseUrl = $"http://localhost:{GetRandomPort()}";
_server = WebHost.CreateDefaultBuilder()
.UseUrls(baseUrl)
.UseStartup<TestStartup>()
.Build();
_server.Start();
Services = _server.Services;
Client = new HttpClient(new WeihanLi.Common.Http.NoProxyHttpClientHandler())
{
BaseAddress = new Uri($"{baseUrl}")
};
// Add Api-Version Header
// Client.DefaultRequestHeaders.TryAddWithoutValidation("Api-Version", "1.2");
Initialize();
Console.WriteLine("test begin");
}
/// <summary>
/// TestDataInitialize
/// </summary>
private void Initialize()
{
}
public void Dispose()
{
using (var dbContext = Services.GetRequiredService<ReservationDbContext>())
{
if (dbContext.Database.IsInMemory())
{
dbContext.Database.EnsureDeleted();
}
}
Client.Dispose();
_server.Dispose();
Console.WriteLine("test end");
}
private static int GetRandomPort()
{
var random = new Random();
var randomPort = random.Next(10000, 65535);
while (IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpListeners().Any(p => p.Port == randomPort))
{
randomPort = random.Next(10000, 65535);
}
return randomPort;
}
}
[CollectionDefinition("APITestCollection")]
public class APITestCollection : ICollectionFixture<APITestFixture>
{
}
- 自定義Collection
[CollectionDefinition("TestCollection")]
public class TestCollection : ICollectionFixture<TestStartupFixture>
{
}
- 自定義一個 TestBase
[Collection("APITestCollection")]
public class ControllerTestBase
{
protected HttpClient Client { get; }
protected IServiceProvider Services { get; }
public ControllerTestBase(APITestFixture fixture)
{
Client = fixture.Client;
Services = fixture.Services;
}
}
- 需要依賴注入的Test類寫法
public class NoticeControllerTest : ControllerTestBase
{
public NoticeControllerTest(APITestFixture fixture) : base(fixture)
{
}
[Fact]
public async Task GetNoticeList()
{
using (var response = await Client.GetAsync("/api/notice"))
{
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var responseString = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<PagedListModel<Notice>>(responseString);
Assert.NotNull(result);
}
}
[Fact]
public async Task GetNoticeDetails()
{
var path = "test-notice";
using (var response = await Client.GetAsync($"/api/notice/{path}"))
{
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var responseString = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<Notice>(responseString);
Assert.NotNull(result);
Assert.Equal(path, result.NoticeCustomPath);
}
}
[Fact]
public async Task GetNoticeDetails_NotFound()
{
using (var response = await Client.GetAsync("/api/notice/test-notice1212"))
{
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
}
}
運行測試,查看我們的 APITestFixture
是不是只實例化了一次,查看輸出日志:
可以看到我們輸出的日志只有一次,說明在整個測試過程中確實只實例化了一次,只會啟動一個 web server,確實是單例的
Memo
微軟推薦的是用 Microsoft.AspNetCore.Mvc.Testing
組件去測試 Controller,但是個人感覺不如自己直接去寫web 服務去測試,如果沒必要引入自己不熟悉的組件最好還是不要去引入新的東西,否則可能就真的是踩坑不止了。