這是一篇借花獻佛、拾人牙慧的文章,之前有寫過這麼一篇文章「單元測試使用 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 類別
如果希望在一個測試方法使用 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 這個關鍵字來去找到各種進階的解決方式。
以上