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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM