如何进行单元测试
测试方法
以下这篇文章详细的说明了单元测试的最佳实践方法,务必仔细阅读,详细了解。 https://learn.microsoft.com/zh-cn/dotnet/core/testing/unit-testing-best-practices 单元测试要求对当前所有修改到部分的代码逻辑需要覆盖到。
测试工具
- dotnet test
dotnet test 命令 - .NET CLI | Microsoft Learn dotnet 命令行中提供了 dotnet test 指令用于执行项目的中测试项目。 可以指定sln、csproj,也可以指定dll,exe 等。 默认是寻找当前目录下的可执行的测试目标。
通常情况下,在项目的根目录下放置sln文件,并引用相关的所有项目,此时执行 dotnet tests
时,会运行当前解决方案下所有测试项目的单元测试。
Fluent Assertions
https://fluentassertions.com/ 单元测试扩展工具,可以使用更流畅的语言声明单元测试。Moq
Moq是最常用的Mock工具,可以用于模拟一切依赖对象。 https://github.com/Moq/moq4/wiki/QuickstartNSubsitute
另外一款Mock工具,可以使用更简洁的语法创建 Mock对象 https://nsubstitute.github.io/Playwright
端到端的测试工具 https://playwright.dev/dotnet/MockQueryable
MockQueryable是一个IQueryable 、 DbSet的模拟工具,可以将List转化为一个DbSet 用于在测试的时候模拟 DbContext ,方便构造数据。 https://github.com/romantitov/MockQueryableBogus
https://github.com/bchavez/Bogus 测试数据生成工具
单元测试覆盖率
将代码覆盖率用于单元测试 - .NET | Microsoft Learn
- 安装报告生成工具
dotnet tool install -g reportgenerator
- 生成测试
dotnet test --collect:"XPlat Code Coverage" --settings coverlet.runsettings
- 生成报告
reportgenerator "-reports:./**/coverage.cobertura.xml" "-targetdir:coveragereport" "-reporttypes:JsonSummary;lcov;Cobertura;Html" -filefilters:"-**\Migrations\*.cs;-**\*.Design.cs"
常用的测试技巧
💡 在此收集测试过程中的方法和技巧,请随意提交分享。
// 如何Mock一个HttpClient
var httpClientMock = new Mock<HttpClient>();
// Set up the SendAsync method to return a desired response
httpClientMock
.Setup(x => x.SendAsync(It.IsAny<HttpRequestMessage>(), CancellationToken.None))
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
// Set up the SendAsync method to match a specific URL and return a desired response
httpClientMock
.Setup(x => x.SendAsync(It.Is<HttpRequestMessage>(req => req.RequestUri.AbsoluteUri == "https://example.com/api/data"), CancellationToken.None))
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("Mocked response") });
// 如何Mock通过IServiceProvider获取的服务
private readonly IServiceProvider provider;
var _db = provider.CreateScope()
.ServiceProvider.GetService<AAADbContext>();
///////
var serviceProvider = new Mock<IServiceProvider>();
var serviceScope = new Mock<IServiceScope>();
serviceScope
.Setup(x =>x.ServiceProvider).Returns(serviceProvider.Object);
var serviceScopeFactory = new Mock<IServiceScopeFactory>();
serviceScopeFactory
.Setup(x =>x.CreateScope()).Returns(serviceScope.Object);
// inject instance
var dbContext = new AAADbContext();
serviceProvider
.Setup(x =>x.GetService(typeof(IServiceScopeFactory)))
.Returns(serviceScopeFactory.Object);
serviceProvider
.Setup(x =>x.GetService(typeof(AAADbContext)))
.Returns(dbContext);
//内存数据库mock事务
var options = new DbContextOptionsBuilder<DatawareDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
var mockDbContext = new Mock<DatawareDbContext>(options)
{
CallBase = true
};
var mockDatabaseFacade = new Mock<DatabaseFacade>(mockDbContext.Object)
{
CallBase = true
};
mockDatabaseFacade.SetupAllProperties();
mockDbContext.Setup(db => db.Database).Returns(mockDatabaseFacade.Object);
// 设置模拟事务的行为
var mockTransaction = new Mock<IDbContextTransaction>();
mockTransaction.Setup(t => t.CommitAsync(It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
mockTransaction.Setup(t => t.RollbackAsync(It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
mockDbContext.Setup(db => db.Database.BeginTransactionAsync(It.IsAny<CancellationToken>()))
.Returns(Task.FromResult(mockTransaction.Object));
// DbContextHelper
public static class DbContextHelper
{
public static AIDbContext CreateDB([CallerMemberName] string dbName = nameof(DbContextHelper))
{
var options = new DbContextOptionsBuilder<AIDbContext>()
.UseInMemoryDatabase(databaseName: dbName)
.Options;
return new AIDbContext(options);
}
public static async Task SeedAsync(this AIDbContext db, params object[] entities)
{
await db.AddRangeAsync(entities);
await db.SaveChangesAsync();
db.ChangeTracker.Clear();
}
}
// MockQueryable
基本示例
var list = new List<>{};
var mockListDbSet = list.AsQueryable().BuildMockDbSet();
mockDbContext.Object.Lists = mockListDbSet.Object;
// 1、Mock _dbContext.AddAsync 方法(_dbContext.Entity<DbSet>.AddAsync不可用)
mockWechatWorkDbContext.Setup(_ => _.AddAsync(It.IsAny<ResourceShareRecord>(), It.IsAny<CancellationToken>()))
.Callback((ResourceShareRecord item, CancellationToken token) =>
{
resourceShareRecords.Add(item);
})
.Returns((ResourceShareRecord model, CancellationToken token) => ValueTask.FromResult((EntityEntry<ResourceShareRecord>)null));
// 2、Mock AddRange方法
mockNoticeDbContext.Setup(_ => _.AddRange(It.IsAny<List<RemindRecord>>()))
.Callback<IEnumerable<object>>((items) =>
{
existRemindRecords.AddRange(items.Select(_ => (RemindRecord)_));
});
// 3、Mock Remove方法
ignoreUsersDbSet.Setup(_ => _.Remove(It.IsAny<IgnoreUser>()))
.Callback<IgnoreUser>((ignoreUser) =>
{
ignoreUsers.Remove(ignoreUser);
});
// 模拟GRpc方法调用
internal static class GrpcCallHelpers
{
public static AsyncUnaryCall<TResponse> CreateAsyncUnaryCall<TResponse>(TResponse response)
{
return new AsyncUnaryCall<TResponse>(
Task.FromResult(response),
Task.FromResult(new Metadata()),
() => Status.DefaultSuccess,
() => new Metadata(),
() => { });
}
public static AsyncUnaryCall<TResponse> CreateAsyncUnaryCall<TResponse>(StatusCode statusCode)
{
var status = new Status(statusCode, string.Empty);
return new AsyncUnaryCall<TResponse>(
Task.FromException<TResponse>(new RpcException(status)),
Task.FromResult(new Metadata()),
() => status,
() => new Metadata(),
() => { });
}
}
// 使用
var mockCall = GrpcCallHelpers.CreateAsyncUnaryCall(new BulkCreateShortLinkResponse
{
Code = 0,
Items = { new List<BulkCreateShortLinkResponseItem>
{
new BulkCreateShortLinkResponseItem
{
Code = 0,
State = "17602106900",
ShortUrl = "2.sc6.me/A6F28764",
LongUrl = "https://www.baidu.com"
}
} }
});
var mockRepository = new MockRepository(MockBehavior.Strict) { DefaultValue = DefaultValue.Mock };
var linkTrackClient = mockRepository.Create<LinkTrackGRpcServiceClient>();
System.Linq.Expressions.Expression<Func<LinkTrackGRpcServiceClient, AsyncUnaryCall<BulkCreateShortLinkResponse>>> expression = x => x.BulkCreateShortLinkAsync(It.IsAny<BulkCreateShortLinkRequest>(), null, null, CancellationToken.None);
linkTrackClient.Setup(expression)
.Returns(mockCall).Verifiable();
// 模拟 HttpContext
// 创建声明和身份
var tenantClaim = new Claim("tenant", tenantId.ToString());
var teamClaim = new Claim("team", "1");
var headers = new HeaderDictionary
{
{ "team", "1" }
};
var identity = new ClaimsIdentity(new[] { tenantClaim, teamClaim }, "testAuthType");
var claimsPrincipal = new ClaimsPrincipal(identity);
// 配置HttpContext模拟
Mock<HttpContext> mockHttpContext = this.mockRepository.Create<HttpContext>();
mockHttpContext.Setup(x => x.User).Returns(claimsPrincipal);
mockHttpContext.Setup(x => x.Request.Headers).Returns(headers);
// 配置到MockIHttpContextAccessor
mockHttpContextAccessor.SetupGet(_ => _.HttpContext).Returns(mockHttpContext.Object);
// 配置到Controller中,做身份认证
var controller = this.CreateAddressBookController();
controller.ControllerContext = new ControllerContext(new ActionContext(mockHttpContext.Object,
new Microsoft.AspNetCore.Routing.RouteData(), new ControllerActionDescriptor()));