單元測試使用 AutoFixture.AutoNSubstitute

介紹在單元測試裡如何使用 AutoFixture.AutoNSubstitute

假設在專案裡有這麼一個類別如下

using AutoMapper;

public class FoobarService : IFoobarbarService
{
    private readonly IMapper _mapper;

    private readonly IFoobarRepository _foobarRepository;

    public FoobarService(IMapper mapper, IFoobarRepository foorbarRepository)
    {
        this._mapper = mapper;
        this._shortUrlReadonlyRepository = shortUrlReadonlyRepository;
    }
	
	...
	...
}

專案裡有使用了 AutoMapper (不過這幾年已經改用 Mapster 了)

如果要對這個類別使用 MSTest  和  NSubsitute 寫單元測試的話,那麼實做出來的大概就會是以下這個樣子

using AutoMapper;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NSubstitute;


[TestClass]
public class FoobarServiceTests
{
    private IMapper _mapper;

    private IFoobarRepository _foobarRepository;

    [TestInitialize]
    public void TestInitialize()
    {
        this._mapper = TestHook.MapperConfigurationProvider.CreateMapper();
        this._foobarRepository = Substitute.For<IfoobarRepository>();
    }

    private FoobarService GetSystemUnderTest()
    {
        var sut = new FoobarService
        (
            this._mapper,
            this._foobarRepository
        );
        return sut;
    }

	...
}	

其中 AutoMapper  的建立則是建立在 TestHook.cs  裡

using AutoMapper;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Sample.Service.Mapping;

[TestClass]
public static class TestHook
{
    private static IConfigurationProvider _mapperConfigurationProvider;

    internal static IConfigurationProvider MapperConfigurationProvider
    {
        get
        {
            return _mapperConfigurationProvider ??= new MapperConfiguration
            (
                options => { options.AddProfile<ServiceMappingProfile>(); }
            );
        }
    }

    [AssemblyInitialize]
    public static void AssemblyInitialize(TestContext context)
    {
        MapperConfigurationProvider.AssertConfigurationIsValid();
    }
}

當類別所相依注入的服務不是很多時,在單元測試裡使用 NSub  建立相依服務的 Stub 還不會很麻煩,

但是當相依服務有些多的時候,在寫單元測試時建立這些相依服務的 Stub 就會有點麻煩 (不過也應該要檢討一下是否職責過多而需要相依很多服務),

尤其是 AutoMapper  這種第三方服務,在單元測試裡還需要在額外多寫一行的指定,寫多了就會很想省下這一點時間,

所以後續我在寫單元測試的時候就會使用 AutoFixture.AutoNSubstitute  這個套件來讓我簡化一些些的處理。

AutoFixture.AutoNSubstitute

https://www.nuget.org/packages/AutoFixture.AutoNSubstitute/

https://github.com/autofixture/autofixture#mocking-libraries

AutoFixture.AutoNSubstitute  是 AutoFixture  所提供的一個 Mocking Library,如果你有使用 xUint  的話,或許對於 AutoFixture  的 AutoData 不會陌生 (AutoFixture.Xunit2)

在測試專案裡安裝好 AutoFixture.AutoNSubstitute 之後就開始改造測試程式碼,首先是 AutoMapper  的 IMapper  的建立,

這裡會使用到 AutoFixtire 裡的 ICustomization  介面

https://github.com/AutoFixture/AutoFixture/blob/master/Src/AutoFixture/ICustomization.cs

namespace AutoFixture
{
    /// <summary>
    /// Encapsulates a customization of an <see cref="IFixture"/>.
    /// </summary>
    public interface ICustomization
    {
        /// <summary>
        /// Customizes the specified fixture.
        /// </summary>
        /// <param name="fixture">The fixture to customize.</param>
        void Customize(IFixture fixture);
    }
}

在測試專案裡繼承 ICustomization 建立 AutoMapperCustomization 類別

using AutoFixture;
using AutoMapper;

namespace Sample.ServiceTests;

public class AutoMapperCustomization : ICustomization
{
    public void Customize(IFixture fixture)
    {
        fixture.Register(() => MapperConfigurationProvider.CreateMapper());
    }

    private static IConfigurationProvider _mapperConfigurationProvider;

    private static IConfigurationProvider MapperConfigurationProvider
    {
        get
        {
            return _mapperConfigurationProvider ??= new MapperConfiguration
            (
                x => x.AddProfile<ServiceMappingProfile>()
            );
        }
    }
}

接著調整測試類別裡建立類別相依服務 Stub  的部分,在網路上看到很多介紹 AutoFixture.AutoNSubstitute 的文章時都會將原本實做類別裡相依服務屬性或欄位的修飾改為 public

例如要改成以下這樣

using AutoMapper;

public class FoobarService : IFoobarbarService
{
    public IMapper _mapper;

    public IFoobarRepository _foobarRepository;

    public FoobarService(IMapper mapper, IFoobarRepository foorbarRepository)
    {
        this._mapper = mapper;
        this._shortUrlReadonlyRepository = shortUrlReadonlyRepository;
    }
	
	...
	...
}

不過改成這樣就破壞了原本類別的封裝性,因為 AutoFixture.AutoNSubstitute 只會針對修飾為公開的欄位、屬性去自動建立 Stub,

我不想因為測試類別要使用 AutoFixture.AutoNSubstitute 簡化程式而去修改原本類別的封裝,所以最後選擇一種折衷的方式來處理,

因為很多服務都會相依使用到 AutoMapper 的 IMapper,所以就建立一個 StubService 基礎類別

using AutoMapper;

namespace Sample.ServiceTests;

public abstract class StubService
{
    /// <summary>
    /// Mapper
    /// </summary>
    public IMapper Mapper { get; set; }
}

再另外建立一個 BaseServiceTestsWithAutoMapper<TStub>  型別並且去繼承 StubService 基礎類別,

在這個 BaseServiceTestsWithAutoMapper<TStub> 類別裡就會使用到 AutoFixture.AutoNSubstitute 的 AutoNSubstituteCustomization  以及我們剛才所建立的 AutoMapperCustomization  類別

類別裡有個 protected 修飾的 IFixture  型別屬性, 在 AutoFixture.Fixtute 的 Customize 方法提供 AutoNSubstituteCustomization instance,增加這個設定後就有 Auto-Mocking 的功能

這麼一來可以在每個測試案例執行時就會透過 AutoFixture.AutoNSubstitute 建立相依服務的 stub

using AutoFixture;
using AutoFixture.AutoNSubstitute;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Sample.ServiceTests;

public abstract class BaseServiceTestsWithAutoMapper<TStub> where TStub : StubService
{
    private IFixture _fixture;

    protected IFixture Fixture => this._fixture ??= new Fixture().Customize(new AutoNSubstituteCustomization())
                                                                 .Customize(new AutoMapperCustomization());

    protected TStub Stub;

    [TestInitialize]
    public void TestInitialize()
    {
        this.Stub = this.Fixture.Create<TStub>();
    }
}

因為不想去破壞原本 FoobarService  類別的封裝,所以我選擇在測試類別裡建立一個很雞肋的 StubFoobarService 類別,

這個類別同樣有著與原本 FoobarService 一樣的欄位、建構式,不過這是給測試用的

public class StubFoobarService : StubService
{
    internal readonly IFoobarRepository FoobarRepository;

    public StubFoobarService(IMapper mapper, IFoobarReadonlyRepository foobarReadonlyRepository)
    {
        this.Mapper = mapper;
        this.FoobarReadonlyRepository = foobarReadonlyRepository;
    }

    internal FoobarService SystemUnderTest => new(this.Mapper, this.FoobarReadonlyRepository);
}

回到 FoobarServiceTests  測試類別裡做最後的改造,FoobarServiceTests  繼承 BaseServiceTestsWithAutoMapper<TStub>,

然後把原本 FoobarServiceTests  裡的 TestInitialize 和 GetSystemUnderTest  方法給移除,再將測試方式裡的程式做調整即可

[TestClass]
public class FoobarServiceTests : BaseServiceTestsWithAutoMapper<StubFoobarService>
{
    //-----------------------------------------------------------------------------------------

    [TestMethod]
    [Owner("Kevin")]
    [TestCategory("FoobarService")]
    [TestProperty("FoobarService", "CreateAsync")]
    public async Task CreateAsync_完成建立_回傳的結果應為true和已建立的資料()
    {
        // arrange
        var sut = this.Stub.SystemUnderTest;

        ...
		...

        this.Stub.FoorbarRepository
            .CreateAsync(Arg.Any<FoobarModel>())
            .Returns(new Result(true) { AffectRows = 1 });

		...
		...

        var model = this.Stub.Mapper.Map<FoobarObject, FoobarModel>(foobar);

		...
		...

        // act
        var actual = await sut.CreateAsync(foobar, new CancellationToken());

        // assert
        actual.Should().NotBeNull();
        actual.Success.Should().BeTrue();
        actual.AffectRows.Should().Be(1);
    }
}

很久沒有寫文章了,除了懶惰之外,另外一個原因是我都在公司裡寫文件、做範例,也就沒有動機與多餘心力再寫部落格文章了。

這一篇文章的內容並不是每個人都能夠接受與認同,就如同我在文章裡所說的「很雞肋」,而且寫法看來也不是很漂亮,

就當成是認識 AutoFixture.AutoNSubstitute  這個套件以及見識見識奇葩寫法吧。

相關連結

分享