.NET单元测试使用AutoFixture按需填充属性的几种方式,以及最佳实践

  • .NET单元测试使用AutoFixture按需填充属性的几种方式,以及最佳实践已关闭评论
  • 55 次浏览
  • A+
所属分类:.NET技术
摘要

AutoFixture是一个.NET库,旨在简化单元测试中的数据设置过程。通过自动生成测试数据,它帮助开发者减少测试代码的编写量,使得单元测试更加简洁、易读和易维护。AutoFixture可以用于任何.NET测试框架,如xUnit、NUnit或MSTest。

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