介紹在單元測試裡如何使用 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 這個套件以及見識見識奇葩寫法吧。