.NET单元测试使用Bogus或AutoFixture按需填充的几种方式和最佳实践
AutoFixture
是一个.NET库,旨在简化单元测试中的数据设置过程。通过自动生成测试数据,它帮助开发者减少测试代码的编写量,使得单元测试更加简洁、易读和易维护。AutoFixture可以用于任何.NET测试框架,如xUnit、NUnit或MSTest。
默认情况下AutoFixture生成的字段值很多时候都满足不了测试需求,比如:
public class User
{
public int Id { get; set; }
public string Name { get; set; } = null!;
[EmailAddress]
public string? Email { get; set; }
[StringLength(512)]
public string? Address { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.Now;
}
如果直接使用 Create<T>()
生成的User对象,他会默认给你填充Id为随机整数,Name和Email为一串Guid,显然这里的邮箱地址生成就不能满足要求,并不是一个有效的邮箱格式
那么如何让AutoFixture按需生成有效的测试数据呢?方法其实有好几种:
方法1:直接定制
var fixture = new Fixture();
fixture.Customize<User>(c => c
.With(x => x.Email, "特定值")
.Without(x => x.Id));
这里,With方法用于指定属性的具体值,而Without方法用于排除某些属性不被自动填充。
方法2:使用匿名函数
这在需要对生成的数据进行更复杂的操作时非常有用。
var fixture = new Fixture();
fixture.Customize<User>(c => c.FromFactory(() => new User
{
Email = "通过工厂方法生成",
}));
方法3:实现ICustomization接口
对于更复杂的定制需求,可以通过实现ICustomization接口来创建一个定制化类。这种方法的好处是可以重用定制逻辑,并且使得测试代码更加整洁。
public class MyCustomClassCustomization : ICustomization
{
public void Customize(IFixture fixture)
{
fixture.Customize<User>(c => c
.With(x => x.Email, "自定义值")
.Without(x => x.Id));
}
}
// 使用定制化
var fixture = new Fixture();
fixture.Customize(new MyCustomClassCustomization());
方法4:使用Build<T>
方法
Build<T>
方法提供了一种链式调用的方式来定制类型的生成规则,这在只需要对单个对象进行简单定制时非常方便。
var myCustomObject = fixture.Build<User>()
.With(x => x.Email, $"{Guid.NewId()}@example.com")
.Without(x => x.Id)
.Create();
最佳实践:
这里以xunit
测试框架为例,
我们需要提前引用AutoFixture
,AutoFixture.Xunit2
库,实现一个UserAutoDataAttribute
类,继承自InlineAutoDataAttribute
重写GetData
方法,大致代码如下:
public class UserAutoDataAttribute : InlineAutoDataAttribute
{
public UserAutoDataAttribute(params object[] values) : base(values)
{
ArgumentNullException.ThrowIfNull(values[0]);
}
public override IEnumerable<object[]> GetData(MethodInfo testMethod)
{
var fixture = new Fixture();
//这里使用上面的4种方式的一种,亦或者根据自身情况定制!
var user = fixture.Build<User>()
//.With(x => x.Id, 0)
.Without(x => x.Id) //ID需要排除因为EFCore需要插入时自动生成
.With(x => x.Email, $"{Uuid7.NewUuid7()}@example.com") //邮箱地址,需要照规则生成
.Create();
yield return new object[] { Values[0], user };
}
}
下面是一个测试用例,需要填充db,和一个自动生成的User参数
public class UnitOfWorkTests(ITestOutputHelper output)
{
[Theory]
[UserAutoData(1)]
[UserAutoData(2)]
public async Task MyUnitOfWorkTest(int db, User user)
{
var services = new ServiceCollection();
services.AddLogging();
services.AddDbContext<TestDbContext>(options =>
{
options.UseInMemoryDatabase($"test-{db}");
});
services.AddUnitOfWork<TestDbContext>();
var provider = services.BuildServiceProvider();
var uow = provider.GetRequiredService<IUnitOfWork<TestDbContext>>();
//add user
await uow.GetRepository<User>().InsertAsync(user);
await uow.SaveChangesAsync();
// select user
var user2 = await uow.GetRepository<User>().FindAsync(1);
Assert.NotNull(user2);
// delete user
uow.GetRepository<User>().Delete(1);
var row = await uow.SaveChangesAsync();
Assert.Equal(1, row);
// select user
user2 = await uow.GetRepository<User>().GetFirstOrDefaultAsync(x => x.Id == 1);
Assert.Null(user2);
}
}
如果你已经习惯编写单元测试,但还没有使用AutoFixture
,那么推荐你尝试一下,也许你也会喜欢上TA
以下是使用Bogus
自动数据相关,因为AutoFixture
填充数据通常数据如果不干预的话,填充的数据都不符合格式要求,而Bogus生成的数据不单单可以指定格式,而且还能根据locale
生成不同文化的数据 因此更加推荐!
以下是Bogus
生成测试数据的两种方式:
public record User(string Name, string Email, string Address, int Age);
//TheoryData<T>
public class MyUserData : TheoryData<User>
{
public MyUserData()
{
var userFaker = new Faker<User>("zh_CN")
.CustomInstantiator(f =>
new User(f.Name.FullName(),
f.Internet.Email("vipwan"),
f.Address.StreetAddress(),
f.Random.Int(18, 99)));
for (int i = 0; i < 5; i++) // 生成 5 个用户数据
{
Add(userFaker.Generate());
}
}
}
//MemberData方式,object[]为弱类型,不推荐这种方式!
public class TestDataGenerator
{
public static IEnumerable<object[]> GetUserData()
{
var userFaker = new Faker<User>("zh_CN")
.CustomInstantiator(f =>
new User(f.Name.FullName(),
f.Internet.Email(),
f.Address.StreetAddress(),
f.Random.Int(18, 99)));
for (int i = 0; i < 5; i++) // 生成 5 个用户数据
{
yield return new object[] { userFaker.Generate() };
}
}
}
以下是测试用例分别使用ClassData
和MemberData
特性填充,这里更推荐使用ClassData
:
[Theory]
[ClassData(typeof(MyUserData))]
public void TestUserIsValid(User user)
{
Assert.NotNull(user);
Assert.False(string.IsNullOrEmpty(user.Name));
Assert.InRange(user.Age, 18, 99);
}
[Theory]
[MemberData(nameof(TestDataGenerator.GetUserData), MemberType = typeof(TestDataGenerator))]
public void TestUserIsValid2(User user)
{
Assert.NotNull(user);
Assert.False(string.IsNullOrEmpty(user.Name));
Assert.InRange(user.Age, 18, 99);
}
两者都是非常有用的库,可以根据你的具体需求和测试场景选择使用。在某些情况下,甚至可以在同一个项目中同时使用 Bogus 和 AutoFixture,以各取所长。
最终如何选择这两种库,我个人是更推荐Bogus
,
以下是两者的一些区别:
Bogus 和 AutoFixture 都是在 .NET 环境中用于生成测试数据的库,但它们在设计理念、使用方式和功能特点上有所不同。
- Bogus
- 设计理念: Bogus 是受到了
JavaScript
的Faker.js
库的启发,专注于生成假数据(mock data),如姓名、地址、邮箱等。它提供了丰富的 API 来定制数据生成规则。 - 使用方式: 在 Bogus 中,你通常会为每种数据类型定义一个“Faker”,通过链式调用来指定如何生成各个属性的值。这种方式在需要高度定制化数据时非常有用。
- 功能特点: Bogus 提供了大量预定义的数据生成方法,支持多种语言和地区设置,使得生成的数据更加真实、多样化。它非常适合于需要生成具体、真实感数据的场景。
- AutoFixture
- 设计理念:
AutoFixture
的设计目标是为自动化测试“减负”,通过减少测试代码中的设置(Arrange)阶段的工作量。它通过约定优于配置的原则,自动为测试方法的参数或对象图提供数据。 - 使用方式: 使用 AutoFixture 时,你不需要显式定义如何生成每个属性的数据。只需创建一个 Fixture 实例,并让它为你的测试数据或对象图填充随机数据。AutoFixture 会尝试理解你的对象构造函数、公共属性等,自动填充数据。
- 功能特点: AutoFixture 的强大之处在于它的自动化能力,能够处理复杂对象图的创建和填充,包括处理循环依赖等问题。它更适合于当你需要快速生成对象或数据以进行测试,但不需要过多关心数据的具体内容时。
- 总结
选择 Bogus :当你需要生成具体、真实感强的数据,或者需要精细控制生成数据的每一个细节。
选择 AutoFixture :当你的主要目标是减少编写测试准备代码的工作量,特别是当测试中涉及复杂对象或对象图时。