使用 AutoFixture.AutoData 來改寫以前的的測試程式碼

這是一篇借花獻佛、拾人牙慧的文章,之前有寫過這麼一篇文章「單元測試使用 AutoFixture.AutoNSubstitute

當時是使用了 AutoFixture.AutoNSubsititute 來減少測試類別裡相依類別的 Stub  建立步驟,用了很奇怪的方式來解決我當時的煩惱。

直到我進入到另一個團隊後,才知道可以藉由 AutoFixture.AutoData 做到更為簡單與彈性的解決方式,這篇就來簡單介紹怎麼做。

AutoFixture.AutoData

https://github.com/AutoFixture/AutoFixture/?tab=readme-ov-file#xunit

AutoFixture.AutoData 在 xUnit 與 NUnit 裡都可以使用,而在 MSTest 裡能不能使用,我就不清楚了。

在 AutoFixture 的 Github Repository README 文件裡就有介紹 AutoData 這個 arrtibute 的功能,但因為介紹的相當簡單,讓當時的我一直以為 AutoFixture.AutoData 只是用來自動產生測試案例裡的測試資料,覺得這個功能蠻雞肋的,所以就沒有深入研究它。

其實早在 2010/10/08 所發佈的一篇文章裡就有說明該怎麼使用

AutoData Theories with AutoFixture by Mark Seemann | ploehj blog

原本測試的實作

藉由建立一個 AutoMoqDataAttribute 類別並繼承 AutoDataAttribute 類別,然後改寫原本的測試案例,並將 [Fact] 改為 [Theory, AutoMoqData],然後在方法裡透過 [Frozen] 取得 經由 AutoMoqData 裡使用 AutoMoqCustomization  所建立的相依物件 Stub,這麼一來就可以在測試方法裡去定義 Stub  的行為和回傳值

 

FrozenAttribute

https://github.com/AutoFixture/AutoFixture/blob/master/Src/AutoFixture.xUnit/FrozenAttribute.cs

AutoFixture.Xunit 中的 FrozenAttribute 用來控制測試中某個類型的實例,在所有需要該類型的地方,都使用同一個已凍結的實例。具體來說,當一個參數被標註為 [Frozen] 時,AutoFixture 將建立這個類別的一個實例並凍結它。隨後在測試方法中,都會使用這個已凍結的實例,而不會建立新的。

FrozenAttribute 提供了方便且靈活的方式來凍結測試中的實例,避免重複建立實例,特別適合有許多相依注入的測試目標類別。這種方式可以保證測試的穩定性和一致性,使測試更加可靠和清晰。

 

建立 AutoDataWithCustomizationAttribute 類別

先來看看要做測試的目標類別

using MapsterMapper;
using Sample.Domain.Entities;
using Sample.Domain.Misc;
using Sample.Domain.Repositories;
using Sample.Domain.Validation;
using Sample.Service.Dto;
using Sample.Service.Interface;
using Throw;

namespace Sample.Service.Implements;

/// <summary>
/// class ShipperService.
/// </summary>
public class ShipperService : IShipperService
{
    private readonly IMapper _mapper;

    private readonly IShipperRepository _shipperRepository;

    /// <summary>
    /// Initializes a new instance of the <see cref="ShipperService"/> class.
    /// </summary>
    /// <param name="mapper">The mapper</param>
    /// <param name="shipperRepository">The shipperRepository</param>
    public ShipperService(IMapper mapper, IShipperRepository shipperRepository)
    {
        this._mapper = mapper;
        this._shipperRepository = shipperRepository;
    }

    //-----------------------------------------------------------------------------------------

    /// <summary>
    /// 以 ShipperId 查詢資料是否存在
    /// </summary>
    /// <param name="shipperId">shipperId</param>
    /// <returns></returns>
    public async Task<bool> IsExistsAsync(int shipperId)
    {
        shipperId.Throw().IfLessThanOrEqualTo(0);
        var exists = await this._shipperRepository.IsExistsAsync(shipperId);
        return exists;
    }

    /// <summary>
    /// 以 ShipperId 查詢資料是否存在
    /// </summary>
    /// <param name="shipperId">shipperId</param>
    /// <returns></returns>
    public async Task<ShipperDto> GetAsync(int shipperId)
    {
        shipperId.Throw().IfLessThanOrEqualTo(0);

        var exists = await this._shipperRepository.IsExistsAsync(shipperId);
        if (!exists)
        {
            return null;
        }

        var model = await this._shipperRepository.GetAsync(shipperId);
        var shipper = this._mapper.Map<ShipperModel, ShipperDto>(model);
        return shipper;
    }

    /// <summary>
    /// 取得 Shipper 的資料總數
    /// </summary>
    /// <returns></returns>
    public async Task<int> GetTotalCountAsync()
    {
        var totalCount = await this._shipperRepository.GetTotalCountAsync();
        return totalCount;
    }

    /// <summary>
    /// 取得所有 Shipper 資料
    /// </summary>
    /// <returns></returns>
    public async Task<IEnumerable<ShipperDto>> GetAllAsync()
    {
        var models = await this._shipperRepository.GetAllAsync();
        var shippers = this._mapper.Map<IEnumerable<ShipperDto>>(models);
        return shippers;
    }

    /// <summary>
    /// 取得所有 Shipper 資料 (分頁)
    /// </summary>
    /// <param name="from">From.</param>
    /// <param name="size">The size.</param>
    /// <returns></returns>
    public async Task<IEnumerable<ShipperDto>> GetCollectionAsync(int @from, int size)
    {
        from.Throw().IfLessThanOrEqualTo(0);
        size.Throw().IfLessThanOrEqualTo(0);

        var totalCount = await this.GetTotalCountAsync();
        if (totalCount.Equals(0))
        {
            return Enumerable.Empty<ShipperDto>();
        }

        if (from > totalCount)
        {
            return Enumerable.Empty<ShipperDto>();
        }

        var models = await this._shipperRepository.GetCollectionAsync(from, size);
        var shippers = this._mapper.Map<IEnumerable<ShipperModel>, IEnumerable<ShipperDto>>(models);
        return shippers;
    }

    /// <summary>
    /// 以 CompanyName or Phone 查詢符合條件的資料
    /// </summary>
    /// <param name="companyName">Name of the company.</param>
    /// <param name="phone">The phone.</param>
    /// <returns></returns>
    public async Task<IEnumerable<ShipperDto>> SearchAsync(string companyName, string phone)
    {
        if (string.IsNullOrWhiteSpace(companyName) && string.IsNullOrWhiteSpace(phone))
        {
            throw new ArgumentException("companyName 與 phone 不可都為空白");
        }

        var totalCount = await this.GetTotalCountAsync();
        if (totalCount.Equals(0))
        {
            return Enumerable.Empty<ShipperDto>();
        }

        var models = await this._shipperRepository.SearchAsync(companyName, phone);
        var shippers = this._mapper.Map<IEnumerable<ShipperDto>>(models);
        return shippers;
    }

    /// <summary>
    /// 新增
    /// </summary>
    /// <param name="shipper">The shipper.</param>
    /// <returns></returns>
    public async Task<IResult> CreateAsync(ShipperDto shipper)
    {
        ModelValidator.Validate(shipper, nameof(shipper));

        var model = this._mapper.Map<ShipperDto, ShipperModel>(shipper);
        var result = await this._shipperRepository.CreateAsync(model);
        return result;
    }

    /// <summary>
    /// 修改
    /// </summary>
    /// <param name="shipper">The shipper.</param>
    /// <returns></returns>
    public async Task<IResult> UpdateAsync(ShipperDto shipper)
    {
        ModelValidator.Validate(shipper, nameof(shipper));

        IResult result = new Result(false);

        var exists = await this._shipperRepository.IsExistsAsync(shipper.ShipperId);
        if (exists is false)
        {
            result.Message = "shipper not exists";
            return result;
        }

        var model = this._mapper.Map<ShipperDto, ShipperModel>(shipper);
        result = await this._shipperRepository.UpdateAsync(model);
        return result;
    }

    /// <summary>
    /// 刪除
    /// </summary>
    /// <param name="shipperId">shipperId</param>
    /// <returns></returns>
    public async Task<IResult> DeleteAsync(int shipperId)
    {
        shipperId.Throw().IfLessThanOrEqualTo(0);

        IResult result = new Result(false);

        var exists = await this._shipperRepository.IsExistsAsync(shipperId);
        if (exists is false)
        {
            result.Message = "shipper not exists";
            return result;
        }

        result = await this._shipperRepository.DeleteAsync(shipperId);
        return result;
    }
}

ShipperService 相依使用 Mapster 的 IMapper 以及 IShipperRepository,而我希望透過建立一個繼承自 AutoDataAttribute 的 AutoDataWithCustomizationAttribute 類別來簡化建立相依物件 Stub 與 SUT 物件(測試目標物件)

在「單元測試使用 AutoFixture.AutoNSubstitute」這篇文章裡就有介紹怎麼繼承 AutoFixture 的 ICustomization 介面並實作一個 AutoMapperCustomization  類別,但這邊因為改用了 Mapster,所以要先來建立 MapsterMapperCustomization 類別

using AutoFixture;
using Mapster;
using MapsterMapper;
using Sample.Service.MapConfig;

namespace Sample.ServiceTests.AutoFixtureConfigurations;

/// <summary>
/// class MapsterMapperCustomization
/// </summary>
public class MapsterMapperCustomization : ICustomization
{
    /// <summary>
    /// Customizes the fixture
    /// </summary>
    /// <param name="fixture">The fixture</param>
    public void Customize(IFixture fixture)
    {
        fixture.Register(() => this.Mapper);
    }

    private IMapper _mapper;

    private IMapper Mapper
    {
        get
        {
            if (this._mapper is not null)
            {
                return this._mapper;
            }

            var typeAdapterConfig = new TypeAdapterConfig();
            typeAdapterConfig.Scan(typeof(ServiceMapRegister).Assembly);
            this._mapper = new Mapper(typeAdapterConfig);
            return this._mapper;
        }
    }
}

再來建立 AutoDataWithCustomizationAttribute 類別,因為我的測試專案是使用 NSubstitute 而不是使用 Moq,所以要在 Fixture 的 Customize 加入 AutoNSubstituteCustomization 實例

using AutoFixture;
using AutoFixture.AutoNSubstitute;
using AutoFixture.Xunit2;

namespace Sample.ServiceTests.AutoFixtureConfigurations;

/// <summary>
/// class AutoDataWithCustomizationAttribute
/// </summary>
/// <seealso cref="AutoDataAttribute"/>
public class AutoDataWithCustomizationAttribute : AutoDataAttribute
{
    /// <summary>
    /// Initializes a new instance of the <see cref="AutoDataWithCustomizationAttribute"/> class
    /// </summary>
    public AutoDataWithCustomizationAttribute() : base(CreateFixture)
    {
    }

    private static IFixture CreateFixture()
    {
        var fixture = new Fixture().Customize(new AutoNSubstituteCustomization())
                                   .Customize(new MapsterMapperCustomization());

        return fixture;
    }
}

 

實作測試類別

首先看第一個測試案例,測試方法上面使用 Theory  與 AutoDataWithCustomization 特性,因為這個測試方法裡沒有要設定相依物件的行為,所以只有透過方法取得透過 AutoData  所建立的測試目標物件,這樣就不用在測試方法裡或是測試類別裡去建立相依類別的 Stub 和測試目標物件 (SUT, System Under Test)

進入 Debug 模式,來觀察這個測試方法執行時的狀況

 

再來看看下面的測試案例,會有需要設定相依類別 Stub 的行為和回傳值,這邊也都是透過 AutoData 去建立,可以在方法簽章裡透過指定 [Frozen] 取得 Stub 然後在 方法裡去設定指定方法執行時的回傳值,甚至連這個回傳值也都是透過 AutoData 所建立

進入 Debug 模式,來觀察這個測試方法執行時的狀況

 

建立 InlineAutoDataWithCustomizationAttribute 類別

https://github.com/AutoFixture/AutoFixture/blob/master/Src/AutoFixture.xUnit/InlineAutoDataAttribute.cs

如果希望在一個測試方法使用 InlineDataAttribute 設定不同的測試情境,然後又希望可以使用到 AutoData 的功能,那麼就要繼承 InlineAutoDataAttribute 介面去實作 InlineAutoDataWithCustomizationAttribute  類別

using AutoFixture;
using AutoFixture.AutoNSubstitute;
using AutoFixture.Xunit2;

namespace Sample.ServiceTests.AutoFixtureConfigurations;

/// <summary>
/// class AutoDataWithCustomizationAttribute
/// </summary>
/// <seealso cref="AutoDataAttribute"/>
public class AutoDataWithCustomizationAttribute : AutoDataAttribute
{
    /// <summary>
    /// Initializes a new instance of the <see cref="AutoDataWithCustomizationAttribute"/> class
    /// </summary>
    public AutoDataWithCustomizationAttribute() : base(CreateFixture)
    {
    }

    private static IFixture CreateFixture()
    {
        var fixture = new Fixture().Customize(new AutoNSubstituteCustomization())
                                   .Customize(new MapsterMapperCustomization());

        return fixture;
    }
}

在測試方法裡的使用

 

完整的測試類別

using AutoFixture;
using AutoFixture.Xunit2;
using FluentAssertions;
using NSubstitute;
using Sample.Domain.Entities;
using Sample.Domain.Misc;
using Sample.Domain.Repositories;
using Sample.Service.Dto;
using Sample.Service.Implements;
using Sample.ServiceTests.AutoFixtureConfigurations;
using Sample.TestResource.AutoFixture;

namespace Sample.ServiceTests.Implements;

public class ShipperServiceTests
{
    //---------------------------------------------------------------------------------------------
    // IsExistsAsync

    [Theory]
    [AutoDataWithCustomization]
    public async Task IsExistsAsync_輸入的ShipperId為0時_應拋出ArgumentOutOfRangeException(ShipperService sut)
    {
        // arrange
        var shipperId = 0;

        // act
        var exception = await Assert.ThrowsAsync<ArgumentOutOfRangeException>(
            () => sut.IsExistsAsync(shipperId));

        // assert
        exception.Message.Should().Contain(nameof(shipperId));
    }

    [Theory]
    [AutoDataWithCustomization]
    public async Task IsExistsAsync_輸入的ShipperId為負1時_應拋出ArgumentOutOfRangeException(ShipperService sut)
    {
        // arrange
        var shipperId = -1;

        // act
        var exception = await Assert.ThrowsAsync<ArgumentOutOfRangeException>(
            () => sut.IsExistsAsync(shipperId));

        // assert
        exception.Message.Should().Contain(nameof(shipperId));
    }

    [Theory]
    [AutoDataWithCustomization]
    public async Task IsExistsAsync_輸入的ShipperId_資料不存在_應回傳false(
        [Frozen] IShipperRepository shipperRepository,
        ShipperService sut)
    {
        // arrange
        var shipperId = 99;

        shipperRepository.IsExistsAsync(Arg.Any<int>()).Returns(false);

        // act
        var actual = await sut.IsExistsAsync(shipperId);

        // assert
        actual.Should().BeFalse();
    }

    [Theory]
    [AutoDataWithCustomization]
    public async Task IsExistsAsync_輸入的ShipperId_資料有存在_應回傳True(
        [Frozen] IShipperRepository shipperRepository,
        ShipperService sut)
    {
        // arrange
        var shipperId = 99;

        shipperRepository.IsExistsAsync(Arg.Any<int>()).Returns(true);

        // act
        var actual = await sut.IsExistsAsync(shipperId);

        // assert
        actual.Should().BeTrue();
    }

    //---------------------------------------------------------------------------------------------
    // GetAsync

    [Theory]
    [AutoDataWithCustomization]
    public async Task GetAsync_輸入的ShipperId為0時_應拋出ArgumentOutOfRangeException(ShipperService sut)
    {
        // arrange
        var shipperId = 0;

        // act
        var exception = await Assert.ThrowsAsync<ArgumentOutOfRangeException>(
            () => sut.GetAsync(shipperId));

        // assert
        exception.Message.Should().Contain(nameof(shipperId));
    }

    [Theory]
    [AutoDataWithCustomization]
    public async Task GetAsync_輸入的ShipperId為負1時_應拋出ArgumentOutOfRangeException(ShipperService sut)
    {
        // arrange
        var shipperId = -1;

        // act
        var exception = await Assert.ThrowsAsync<ArgumentOutOfRangeException>(
            () => sut.GetAsync(shipperId));

        // assert
        exception.Message.Should().Contain(nameof(shipperId));
    }

    [Theory]
    [AutoDataWithCustomization]
    public async Task GetAsync_輸入的ShipperId_資料不存在_應回傳null(
        [Frozen] IShipperRepository shipperRepository,
        ShipperService sut)
    {
        // arrange
        var shipperId = 99;

        shipperRepository.IsExistsAsync(Arg.Any<int>()).Returns(false);

        // act
        var actual = await sut.GetAsync(shipperId);

        // assert
        actual.Should().BeNull();
    }

    [Theory]
    [AutoDataWithCustomization]
    public async Task GetAsync_輸入的ShipperId_資料有存在_應回傳model(
        [Frozen] IShipperRepository shipperRepository,
        ShipperService sut,
        ShipperModel model)
    {
        // arrange
        var shipperId = model.ShipperId;

        shipperRepository.IsExistsAsync(Arg.Any<int>()).Returns(true);
        shipperRepository.GetAsync(Arg.Any<int>()).Returns(model);

        // act
        var actual = await sut.GetAsync(shipperId);

        // assert
        actual.Should().NotBeNull();
        actual.ShipperId.Should().Be(shipperId);
    }

    //---------------------------------------------------------------------------------------------
    // GetTotalCountAsync

    [Theory]
    [AutoDataWithCustomization]
    public async Task GetTotalCountAsync_資料表裡無資料_應回傳0(ShipperService sut)
    {
        // arrange
        var expected = 0;

        // act
        var actual = await sut.GetTotalCountAsync();

        // assert
        actual.Should().Be(expected);
    }

    [Theory]
    [AutoDataWithCustomization]
    public async Task GetTotalCountAsync_資料表裡有10筆資料_應回傳10(
        [Frozen] IShipperRepository shipperRepository,
        ShipperService sut)
    {
        // arrange
        var expected = 10;

        shipperRepository.GetTotalCountAsync().Returns(10);

        // act
        var actual = await sut.GetTotalCountAsync();

        // assert
        actual.Should().Be(expected);
    }

    //---------------------------------------------------------------------------------------------
    // GetAllAsync

    [Theory]
    [AutoDataWithCustomization]
    public async Task GetAllAsync_資料表裡無資料_應回傳空集合(ShipperService sut)
    {
        // arrange

        // act
        var actual = await sut.GetAllAsync();

        // assert
        actual.Should().BeEmpty();
    }

    [Theory]
    [AutoDataWithCustomization]
    public async Task GetAllAsync_資料表裡有10筆資料_回傳的集合裡有10筆(
        [Frozen] IShipperRepository shipperRepository,
        ShipperService sut,
        [CollectionSize(10)] IEnumerable<ShipperModel> models)
    {
        // arrange
        shipperRepository.GetAllAsync().Returns(models);

        // act
        var actual = await sut.GetAllAsync();

        // assert
        actual.Should().NotBeEmpty();
        actual.Should().HaveCount(10);
    }

    //---------------------------------------------------------------------------------------------
    // GetCollectionAsync

    [Theory]
    [InlineAutoDataWithCustomization(0, 10, nameof(from))]
    [InlineAutoDataWithCustomization(-1, 10, nameof(from))]
    [InlineAutoDataWithCustomization(1, 0, nameof(size))]
    [InlineAutoDataWithCustomization(1, -1, nameof(size))]
    public async Task GetCollectionAsync_from與size輸入不合規格內容_應拋出ArgumentOutOfRangeException(
        int from, int size, string parameterName, ShipperService sut)
    {
        // act
        var exception = await Assert.ThrowsAsync<ArgumentOutOfRangeException>(
            () => sut.GetCollectionAsync(from, size));

        // assert
        exception.Message.Should().Contain(parameterName);
    }

    [Theory]
    [AutoDataWithCustomization]
    public async Task GetCollectionAsync_from為1_size為10_資料表裡無資料_應回傳空集合(
        [Frozen] IShipperRepository shipperRepository,
        ShipperService sut)
    {
        // arrange
        const int from = 1;
        const int size = 10;

        shipperRepository.GetTotalCountAsync().Returns(0);

        // act
        var actual = await sut.GetCollectionAsync(from, size);

        // assert
        actual.Should().BeEmpty();
    }

    [Theory]
    [AutoDataWithCustomization]
    public async Task GetCollectionAsync_from為20_size為10_資料表裡只有10筆資料_from超過總數量_應回傳空集合(
        [Frozen] IShipperRepository shipperRepository,
        ShipperService sut)
    {
        // arrange
        const int from = 20;
        const int size = 10;

        shipperRepository.GetTotalCountAsync().Returns(10);

        // act
        var actual = await sut.GetCollectionAsync(from, size);

        // assert
        actual.Should().BeEmpty();
    }

    [Theory]
    [AutoDataWithCustomization]
    public async Task GetCollectionAsync_from為1_size為10_資料表裡有5筆資料_回傳集合應有5筆(
        [Frozen] IShipperRepository shipperRepository,
        ShipperService sut,
        [CollectionSize(5)] IEnumerable<ShipperModel> models)
    {
        // arrange
        const int from = 1;
        const int size = 10;

        shipperRepository.GetTotalCountAsync().Returns(5);

        shipperRepository
            .GetCollectionAsync(Arg.Any<int>(), Arg.Any<int>())
            .Returns(models);

        // act
        var actual = await sut.GetCollectionAsync(from, size);

        // assert
        actual.Should().NotBeEmpty();
        actual.Should().HaveCount(5);
    }

    [Theory]
    [AutoDataWithCustomization]
    public async Task GetCollectionAsync_from為6_size為10_資料表裡有10筆資料_回傳集合應有10筆(
        [Frozen] IShipperRepository shipperRepository,
        ShipperService sut,
        [CollectionSize(20)] IEnumerable<ShipperModel> models)
    {
        // arrange
        const int from = 6;
        const int size = 10;

        shipperRepository.GetTotalCountAsync().Returns(10);

        shipperRepository
            .GetCollectionAsync(Arg.Any<int>(), Arg.Any<int>())
            .Returns(models.Skip(5).Take(10));

        // act
        var actual = await sut.GetCollectionAsync(from, size);

        // assert
        actual.Should().NotBeEmpty();
        actual.Should().HaveCount(10);
    }

    [Theory]
    [AutoDataWithCustomization]
    public async Task GetCollectionAsync_from為11_size為10_資料表裡有30筆資料_回傳集合應有10筆(
        [Frozen] IShipperRepository shipperRepository,
        ShipperService sut,
        [CollectionSize(30)] IEnumerable<ShipperModel> models)
    {
        // arrange
        const int from = 11;
        const int size = 10;

        shipperRepository.GetTotalCountAsync().Returns(30);

        shipperRepository
            .GetCollectionAsync(Arg.Any<int>(), Arg.Any<int>())
            .Returns(models.Skip(10).Take(10));

        // act
        var actual = await sut.GetCollectionAsync(from, size);

        // assert
        actual.Should().NotBeEmpty();
        actual.Should().HaveCount(10);
    }

    //---------------------------------------------------------------------------------------------
    // SearchAsync

    [Theory]
    [InlineAutoDataWithCustomization(null, null)]
    [InlineAutoDataWithCustomization("", null)]
    [InlineAutoDataWithCustomization(null, "")]
    [InlineAutoDataWithCustomization(null, null)]
    public async Task SearchAsync_companyName與phone輸入不合規格的內容_應拋出ArgumentException(
        string companyName, string phone, ShipperService sut)
    {
        // arrange
        const string exceptionMessage = "companyName 與 phone 不可都為空白";

        // act
        var exception = await Assert.ThrowsAsync<ArgumentException>(
            () => sut.SearchAsync(companyName, phone));

        // assert
        exception.Message.Should().Be(exceptionMessage);
    }

    [Theory]
    [AutoDataWithCustomization]
    public async Task SearchAsync_資料表裡無資料_應回傳空集合(
        [Frozen] IShipperRepository shipperRepository,
        ShipperService sut)
    {
        // arrange
        const string companyName = "test";
        const string phone = "02123456789";

        shipperRepository.GetTotalCountAsync().Returns(0);

        // act
        var actual = await sut.SearchAsync(companyName, phone);

        // assert
        actual.Should().BeEmpty();
    }

    [Theory]
    [AutoDataWithCustomization]
    public async Task SearchAsync_companyName輸入資料_沒有符合條件的資料_應回傳空集合(
        [Frozen] IShipperRepository shipperRepository,
        ShipperService sut)
    {
        // arrange
        const string companyName = "test";
        const string phone = "";

        shipperRepository.GetTotalCountAsync().Returns(10);

        shipperRepository
            .SearchAsync(Arg.Any<string>(), Arg.Any<string>())
            .Returns(Enumerable.Empty<ShipperModel>());

        // act
        var actual = await sut.SearchAsync(companyName, phone);

        // assert
        actual.Should().BeEmpty();
    }

    [Theory]
    [AutoDataWithCustomization]
    public async Task SearchAsync_companyName輸入資料_phone無輸入_有符合條件的資料_回傳集合應包含符合條件的資料(
        IFixture fixture,
        [Frozen] IShipperRepository shipperRepository,
        ShipperService sut)
    {
        // arrange
        var models = fixture.Build<ShipperModel>()
                            .With(x => x.CompanyName, "test")
                            .CreateMany(1);

        shipperRepository.GetTotalCountAsync().Returns(10);

        shipperRepository.SearchAsync(Arg.Any<string>(), Arg.Any<string>())
                         .Returns(models);

        const string companyName = "test";
        const string phone = "";

        // act
        var actual = await sut.SearchAsync(companyName, phone);

        // assert
        actual.Should().NotBeEmpty();
        actual.Should().HaveCount(1);
        actual.Any(x => x.CompanyName == companyName).Should().BeTrue();
    }

    [Theory]
    [AutoDataWithCustomization]
    public async Task SearchAsync_companyName無輸入_phone輸入資料_有符合條件的資料_回傳集合應包含符合條件的資料(
        IFixture fixture,
        [Frozen] IShipperRepository shipperRepository,
        ShipperService sut)
    {
        // arrange
        var models = fixture.Build<ShipperModel>()
                            .With(x => x.Phone, "02123456789")
                            .CreateMany(1);

        shipperRepository.GetTotalCountAsync().Returns(10);

        shipperRepository.SearchAsync(Arg.Any<string>(), Arg.Any<string>())
                         .Returns(models);

        const string companyName = "";
        const string phone = "02123456789";

        // act
        var actual = await sut.SearchAsync(companyName, phone);

        // assert
        actual.Should().NotBeEmpty();
        actual.Should().HaveCount(1);
        actual.Any(x => x.Phone == phone).Should().BeTrue();
    }

    [Theory]
    [AutoDataWithCustomization]
    public async Task SearchAsync_companyName輸入資料_phone輸入資料_有符合條件的資料_回傳集合應包含符合條件的資料(
        IFixture fixture,
        [Frozen] IShipperRepository shipperRepository,
        ShipperService sut)
    {
        // arrange
        var models = fixture.Build<ShipperModel>()
                            .With(x => x.CompanyName, "demo")
                            .With(x => x.Phone, "03123456789")
                            .CreateMany(1);

        shipperRepository.GetTotalCountAsync().Returns(11);

        shipperRepository.SearchAsync(Arg.Any<string>(), Arg.Any<string>())
                         .Returns(models);

        const string companyName = "demo";
        const string phone = "03123456789";

        // act
        var actual = await sut.SearchAsync(companyName, phone);

        // assert
        actual.Should().NotBeEmpty();
        actual.Should().HaveCount(1);
        actual.Any(x => x.CompanyName == companyName && x.Phone == phone).Should().BeTrue();
    }

    [Theory]
    [AutoDataWithCustomization]
    public async Task SearchAsync_companyName輸入資料_phone輸入資料_沒有符合條件的資料_應回傳空集合(
        [Frozen] IShipperRepository shipperRepository,
        ShipperService sut)
    {
        // arrange
        shipperRepository.GetTotalCountAsync().Returns(10);

        shipperRepository.SearchAsync(Arg.Any<string>(), Arg.Any<string>())
                         .Returns(Enumerable.Empty<ShipperModel>());

        const string companyName = "try";
        const string phone = "04123456789";

        // act
        var actual = await sut.SearchAsync(companyName, phone);

        // assert
        actual.Should().BeEmpty();
    }

    [Theory]
    [AutoDataWithCustomization]
    public async Task SearchAsync_companyName輸入資料_phone無輸入_有2筆符合條件的資料_回傳集合應有兩筆(
        IFixture fixture,
        [Frozen] IShipperRepository shipperRepository,
        ShipperService sut)
    {
        // arrange
        var model1 = fixture.Build<ShipperModel>()
                            .With(x => x.CompanyName, "note")
                            .Create();

        var model2 = fixture.Build<ShipperModel>()
                            .With(x => x.CompanyName, "node")
                            .Create();

        var models = new List<ShipperModel>
        {
            model1,
            model2
        };

        shipperRepository.GetTotalCountAsync().Returns(10);

        shipperRepository.SearchAsync(Arg.Any<string>(), Arg.Any<string>())
                         .Returns(models);

        const string companyName = "no";
        const string phone = "";

        // act
        var actual = await sut.SearchAsync(companyName, phone);

        // assert
        actual.Should().NotBeEmpty();
        actual.Should().HaveCount(2);
        actual.All(x => x.CompanyName.StartsWith("no")).Should().BeTrue();
    }

    //---------------------------------------------------------------------------------------------
    // CreateAsync

    [Theory]
    [AutoDataWithCustomization]
    public async Task CreateAsync_輸入的model為null時_應拋出ArgumentNullException(ShipperService sut)
    {
        // arrange
        ShipperDto shipperDto = null;

        // act
        var exception = await Assert.ThrowsAsync<ArgumentNullException>(
            () => sut.CreateAsync(shipperDto));

        // assert
        exception.Message.Should().Contain("shipper");
    }

    [Theory]
    [AutoDataWithCustomization]
    public async Task CreateAsync_輸入一個有資料的model_新增完成_回傳Result的Success應為true(
        [Frozen] IShipperRepository shipperRepository,
        ShipperService sut,
        ShipperDto shipperDto)
    {
        // arrange
        shipperRepository.CreateAsync(Arg.Any<ShipperModel>())
                         .Returns(new Result { Success = true, AffectRows = 1 });

        // act
        var actual = await sut.CreateAsync(shipperDto);

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

    //---------------------------------------------------------------------------------------------
    // UpdateAsync

    [Theory]
    [AutoDataWithCustomization]
    public async Task UpdateAsync_輸入的model為null時_應拋出ArgumentNullException(ShipperService sut)
    {
        // arrange
        ShipperDto shipperDto = null;

        // act
        var exception = await Assert.ThrowsAsync<ArgumentNullException>(
            () => sut.UpdateAsync(shipperDto));

        // assert
        exception.Message.Should().Contain("shipper");
    }

    [Theory]
    [AutoDataWithCustomization]
    public async Task UpdateAsync_輸入model_要修改的資料並不存在_更新錯誤_回傳Result的Success應為false(
        [Frozen] IShipperRepository shipperRepository,
        ShipperService sut,
        ShipperDto shipperDto)
    {
        // arrange
        shipperRepository.IsExistsAsync(Arg.Any<int>()).Returns(false);

        // act
        var actual = await sut.UpdateAsync(shipperDto);

        // assert
        actual.Success.Should().BeFalse();
        actual.Message.Should().Be("shipper not exists");
    }

    [Theory]
    [AutoDataWithCustomization]
    public async Task UpdateAsync_輸入model_要修改的資料存在_更新完成_回傳Result的Success應為true(
        [Frozen] IShipperRepository shipperRepository,
        ShipperService sut,
        ShipperDto shipperDto)
    {
        // arrange
        shipperDto.CompanyName = "update";

        shipperRepository.IsExistsAsync(Arg.Any<int>()).Returns(true);

        shipperRepository.UpdateAsync(Arg.Any<ShipperModel>())
                         .Returns(new Result { Success = true, AffectRows = 1 });

        // act
        var actual = await sut.UpdateAsync(shipperDto);

        // assert
        actual.Success.Should().BeTrue();
    }

    //---------------------------------------------------------------------------------------------
    // DeleteAsync

    [Theory]
    [AutoDataWithCustomization]
    public async Task DeleteAsync_輸入的ShipperId為0時_應拋出ArgumentOutOfRangeException(ShipperService sut)
    {
        // arrange
        var shipperId = 0;

        // act
        var exception = await Assert.ThrowsAsync<ArgumentOutOfRangeException>(
            () => sut.DeleteAsync(shipperId));

        // assert
        exception.Message.Should().Contain(nameof(shipperId));
    }

    [Theory]
    [AutoDataWithCustomization]
    public async Task DeleteAsync_輸入的ShipperId為負1時_應拋出ArgumentOutOfRangeException(ShipperService sut)
    {
        // arrange
        var shipperId = -1;

        // act
        var exception = await Assert.ThrowsAsync<ArgumentOutOfRangeException>(
            () => sut.DeleteAsync(shipperId));

        // assert
        exception.Message.Should().Contain(nameof(shipperId));
    }

    [Theory]
    [AutoDataWithCustomization]
    public async Task DeleteAsync_輸入ShipperId_要刪除的資料並不存在_刪除錯誤_回傳Result的Success應為false(
        [Frozen] IShipperRepository shipperRepository,
        ShipperService sut)
    {
        // arrange
        var shipperId = 999;

        shipperRepository.IsExistsAsync(Arg.Any<int>()).Returns(false);

        // act
        var actual = await sut.DeleteAsync(shipperId);

        // assert
        actual.Success.Should().BeFalse();
        actual.Message.Should().Be("shipper not exists");
    }

    [Theory]
    [AutoDataWithCustomization]
    public async Task DeleteAsync_輸入model_要刪除的資料存在_刪除完成_回傳Result的Success應為true(
        [Frozen] IShipperRepository shipperRepository,
        ShipperService sut,
        ShipperDto shipperDto)
    {
        // arrange
        shipperRepository.IsExistsAsync(Arg.Any<int>()).Returns(true);

        shipperRepository.DeleteAsync(Arg.Any<int>())
                         .Returns(new Result { Success = true, AffectRows = 1 });

        // act
        var actual = await sut.DeleteAsync(shipperDto.ShipperId);

        // assert
        actual.Success.Should().BeTrue();
    }
}

 

這篇只是簡單介紹 AutoFixture.AutoData 的應用,當然實際專案裡可能會有遇到更為複雜的設定,在網路上都可以透過 AutoData 這個關鍵字來去找到各種進階的解決方式。

以上