You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 08:57:12