如何为Handler方法编写单元测试?解决连接字符串未初始化问题
解决方案
1. 定义接口解耦(最优方案)
Moq无法直接Mock非虚方法的具体类,所以核心是给DogTrailService抽象出接口,让CommandHandler依赖抽象而非具体实现,彻底隔离数据库操作。
步骤1:创建IDogTrailService接口
public interface IDogTrailService { Task<DogTrail> AddAsync(DogTrail entity); }
步骤2:让DogTrailService实现接口
修正原代码中的拼写错误(类名与构造函数名一致、SQL类名大小写、SQL语句语法):
public class DogTrailService : IDogTrailService { private readonly string _dogTrailConnectionString; // 修正构造函数名与类名一致 public DogTrailService(IConfiguration configuration) { _dogTrailConnectionString = configuration.GetConnectionString("DogTrailSqlDb"); } public async Task<DogTrail> AddAsync(DogTrail entity) { // 修正SqlConnection大小写 using(SqlConnection connection = new SqlConnection(_dogTrailConnectionString)) { await connection.OpenAsync(); using(SqlTransaction transaction = connection.BeginTransaction()) { // 修正SQL语句的分号与冒号错误 string query = "INSERT INTO DogTrail (dognumb, dogIndex) VALUES (@dognumb, @dogindex); SELECT SCOPE_IDENTITY();"; // 修正SqlCommand大小写,添加事务参数 using(SqlCommand command = new SqlCommand(query, connection, transaction)) { // 修正参数拼写错误 command.Parameters.AddWithValue("@dognumb", entity.dognumb); command.Parameters.AddWithValue("@dogindex", entity.dogindex); entity.Id = (int)(decimal)await command.ExecuteScalarAsync(); transaction.Commit(); return entity; } } } } }
步骤3:修改CommandHandler的依赖
让Handler依赖接口而非具体类,同时修正原代码的拼写错误:
private readonly IMapper _mapper; private readonly ILogger<CreateDogTrailItemCommandHandler> _logger; private readonly IDogTrailService _dogTrailService; // 修正IMapper拼写错误,调整依赖类型为接口 public CreateDogTrailCommandHandler(IMapper mapper, IDogTrailService dogTrailService, ILogger<CreateDogTrailItemCommandHandler> dogLogger) { _dogTrailService = dogTrailService; _mapper = mapper; _logger = dogLogger; } public async Task<Unit> Handle(CreateDogTrailItemCommand request, CancellationToken cancellationToken) { var dogTrailItem = _mapper.Map<DogTrail>(request.DogTrailDto); await _dogTrailService.AddAsync(dogTrailItem); // 修正Unit.Value大小写 return Unit.Value; }
步骤4:编写单元测试(Mock接口)
现在可以完全Mock接口,避免调用真实数据库逻辑:
[Test] public async Task Handle_ValidRequest_AddsNewDogTrailItem() { // Arrange var mockMapper = new Mock<IMapper>(); var mockDogTrailService = new Mock<IDogTrailService>(); var mockLogger = new Mock<ILogger<CreateDogTrailItemCommandHandler>>(); var handler = new CreateDogTrailItemCommandHandler( mockMapper.Object, mockDogTrailService.Object, mockLogger.Object); var requestDto = new DogTrailDto(); var request = new CreateDogTrailItemCommand(requestDto); var mappedDogTrail = new DogTrail(); mockMapper.Setup(m => m.Map<DogTrail>(requestDto)).Returns(mappedDogTrail); mockDogTrailService.Setup(s => s.AddAsync(mappedDogTrail)).ReturnsAsync(mappedDogTrail); // Act await handler.Handle(request, CancellationToken.None); // Assert mockMapper.Verify(m => m.Map<DogTrail>(requestDto), Times.Once); mockDogTrailService.Verify(s => s.AddAsync(mappedDogTrail), Times.Once); }
2. 临时替代方案(不定义接口)
如果暂时不想调整代码结构,可以将AddAsync改为虚方法,让Moq能够Mock该方法:
步骤1:修改AddAsync为虚方法
public virtual async Task<DogTrail> AddAsync(DogTrail entity) { // 原方法逻辑不变 }
步骤2:调整测试代码
[Test] public async Task Handle_ValidRequest_AddsNewDogTrailItem() { // Arrange var mockMapper = new Mock<IMapper>(); // 模拟配置对象,避免构造函数报错 var mockConfig = new Mock<IConfiguration>(); mockConfig.Setup(c => c.GetConnectionString("DogTrailSqlDb")).Returns("FakeConnectionString"); var mockDogTrailService = new Mock<DogTrailService>(mockConfig.Object); // Mock虚方法,跳过真实数据库操作 mockDogTrailService.Setup(s => s.AddAsync(It.IsAny<DogTrail>())).ReturnsAsync(new DogTrail()); var mockLogger = new Mock<ILogger<CreateDogTrailItemCommandHandler>>(); var handler = new CreateDogTrailItemCommandHandler( mockMapper.Object, mockDogTrailService.Object, mockLogger.Object); var request = new CreateDogTrailItemCommand(new DogTrailDto()); var dogTrail = new DogTrail(); mockMapper.Setup(m => m.Map<DogTrail>(It.IsAny<DogTrailDto>())).Returns(dogTrail); // Act await handler.Handle(request, CancellationToken.None); // Assert mockMapper.Verify(m => m.Map<DogTrail>(request.DogTrailDto), Times.Once); mockDogTrailService.Verify(s => s.AddAsync(dogTrail), Times.Once); }
注意:此方案仅为临时 workaround,长期来看定义接口的方式更符合依赖倒置原则,可维护性与可测试性更强。
核心原则
单元测试的本质是隔离外部依赖,数据库属于外部资源,测试时绝对不应调用真实数据库,必须通过Mock模拟服务行为。
内容的提问来源于stack exchange,提问作者LowKeyLo
相关产品推荐
相关产品推荐

