XUnit 依赖注入

0
12

XUnit 依赖注入

Intro

现在的开发中越来越看重依赖注入的思想,微软的 Asp.Net Core 框架更是天然集成了依赖注入,那么在单元测试中如何使用依赖注入呢?

本文主要介绍如何通过 XUnit 来实现依赖注入, XUnit 主要借助 SharedContext 来共享一部分资源包括这些资源的创建以及释放。

Scoped

针对 Scoped 的对象可以借助 XUnit 中的 IClassFixture 来实现

  1. 定义自己的 Fixture,需要初始化的资源在构造方法里初始化,如果需要在测试结束的时候释放资源需要实现 IDisposable 接口
  2. 需要依赖注入的测试类实现接口 IClassFixture<Fixture>
  3. 在构造方法中注入实现的 Fixture 对象,并在构造方法中使用 Fixture 对象中暴露的公共成员

Singleton

针对 Singleton 的对象可以借助 XUnit 中的 ICollectionFixture 来实现

  1. 定义自己的 Fixture,需要初始化的资源在构造方法里初始化,如果需要在测试结束的时候释放资源需要实现 IDisposable 接口
  2. 创建 CollectionDefinition,实现接口 ICollectionFixture<Fixture>,并添加一个 [CollectionDefinition("CollectionName")] Attribute,CollectionName 需要在整个测试中唯一,不能出现重复的 CollectionName
  3. 在需要注入的测试类中添加 [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 的测试为例

  1. 自定义 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>
{
}
  1. 自定义Collection
[CollectionDefinition("TestCollection")]
public class TestCollection : ICollectionFixture<TestStartupFixture>
{
}
  1. 自定义一个 TestBase
[Collection("APITestCollection")]
public class ControllerTestBase
{
    protected HttpClient Client { get; }

    protected IServiceProvider Services { get; }

    public ControllerTestBase(APITestFixture fixture)
    {
        Client = fixture.Client;
        Services = fixture.Services;
    }
}
  1. 需要依赖注入的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

<

发布回复

请输入评论!
请输入你的名字