Table of Contents

如何进行单元测试

测试方法

以下这篇文章详细的说明了单元测试的最佳实践方法,务必仔细阅读,详细了解。 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 时,会运行当前解决方案下所有测试项目的单元测试。

单元测试覆盖率

将代码覆盖率用于单元测试 - .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()));