如何用NetArchTest实现.NET架构测试,强制仓储类公开方法使用Serilog日志
在.NET 8 Web API中用NetArchTest.Rules验证仓储类日志规范
前置准备
先在测试项目中安装NetArchTest.Rules NuGet包:
dotnet add package NetArchTest.Rules
架构测试实现
我们需要编写两个核心测试,确保仓储类的所有公开非构造方法都包含符合要求的入口日志和出口日志。
测试逻辑说明
- 定位目标类:通过命名空间或接口实现,筛选项目中所有仓储类
- 入口日志验证:检查每个公开方法开头是否调用
_logger.Information(),且日志消息包含当前方法名 - 出口日志验证:检查每个公开方法返回前是否调用
_logger.Information(),且日志消息包含方法名、参数或结果相关信息
完整测试代码示例(xUnit)
using System.Reflection; using System.Linq; using Mono.Cecil.Cil; using NetArchTest.Rules; using Xunit; using Serilog; namespace Demo.Tests.Architecture { public class RepositoryLoggingEnforcementTests { // 指向仓储所在的程序集 private readonly Assembly _repositoryAssembly = typeof(Demo.Repository.ProductRepository).Assembly; [Fact] public void Repository_Public_Methods_Must_Contain_Entry_Logging() { var testResult = Types.InAssembly(_repositoryAssembly) .That() .AreClasses() .And() .InNamespace("Demo.Repository") // 按命名空间筛选仓储类 .Should() .HavePublicMethods(methods => methods.Where(m => !m.IsConstructor) // 排除构造函数 .Each(method => { // 检查方法体是否存在Serilog的Information调用,且日志包含当前方法名 var hasValidEntryLog = method.MethodBody.Instructions .Any(inst => inst.OpCode.Name == "callvirt" && inst.Operand is MethodInfo logMethod && logMethod.Name == "Information" && IsSerilogLoggerMethod(logMethod) && method.MethodBody.Instructions .Any(i => i.Operand is string msg && msg.Contains(method.Name))); return hasValidEntryLog; })) .GetResult(); Assert.True(testResult.IsSuccessful, string.Join("\n", testResult.FailingTypeNames.Select(name => $"{name}: 缺少符合规范的入口日志"))); } [Fact] public void Repository_Public_Methods_Must_Contain_Exit_Logging() { var testResult = Types.InAssembly(_repositoryAssembly) .That() .AreClasses() .And() .InNamespace("Demo.Repository") .Should() .HavePublicMethods(methods => methods.Where(m => !m.IsConstructor) .Each(method => { // 找到所有return指令,检查return前的最后一个调用是否是Serilog的Information var returnInstructions = method.MethodBody.Instructions .Where(inst => inst.OpCode == OpCodes.Ret); foreach (var retInst in returnInstructions) { var prevCallInst = method.MethodBody.Instructions .TakeWhile(inst => inst != retInst) .Reverse() .FirstOrDefault(inst => inst.OpCode.Name == "callvirt"); if (prevCallInst == null) return false; if (prevCallInst.Operand is MethodInfo logMethod && logMethod.Name == "Information" && IsSerilogLoggerMethod(logMethod)) { // 检查日志消息包含方法名或参数/结果占位符 var msgInstruction = method.MethodBody.Instructions .TakeWhile(inst => inst != prevCallInst) .Reverse() .FirstOrDefault(i => i.Operand is string); if (msgInstruction != null) { var logMsg = (string)msgInstruction.Operand; if (logMsg.Contains(method.Name) || logMsg.Contains("{0}") || logMsg.Contains("{id}")) return true; } } } return false; })) .GetResult(); Assert.True(testResult.IsSuccessful, string.Join("\n", testResult.FailingTypeNames.Select(name => $"{name}: 缺少符合规范的出口日志"))); } // 辅助方法:确认是Serilog的ILogger.Information方法 private bool IsSerilogLoggerMethod(MethodInfo method) { var declaringType = method.DeclaringType; return declaringType == typeof(ILogger) || (declaringType.IsGenericType && declaringType.GetGenericTypeDefinition() == typeof(ILogger<>)); } } }
关键细节说明
- 仓储类筛选:示例用命名空间筛选,你也可以替换为
.ImplementInterface(typeof(IRepository)),匹配所有实现仓储接口的类 - 异步方法适配:如果仓储包含异步方法,需要调整return指令的检查逻辑,比如查找
await后的ret指令,或直接校验方法体末尾的日志调用 - 日志内容校验:示例仅做基础校验,你可以根据团队规范进一步严格要求(比如强制包含参数名、结果状态描述等)
运行测试
直接在测试项目中执行这些架构测试,若有仓储方法不符合日志规范,测试会失败并输出具体的类名和问题描述。
内容的提问来源于stack exchange,提问作者santosh kumar patro
相关产品推荐
相关产品推荐

