Loading

XUnit 依赖注入

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

posted @ 2018-10-08 17:18  WeihanLi  阅读(1956)  评论(4编辑  收藏  举报