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 服务去测试,如果没必要引入自己不熟悉的组件最好还是不要去引入新的东西,否则可能就真的是踩坑不止了。
Reference
https://xunit.github.io/docs/shared-context.html
https://github.com/WeihanLi/ActivityReservation/tree/dev/ActivityReservation.API.Test