使用 NUnit/NSubstitute/AutoFixture 和 InsightDatabase 模拟 DbConnection

Mocking a DbConnection with NUnit/NSubstitute/AutoFixture and InsightDatabase

我们正在使用 Nunit、NSubstitute 和 AutoFixture 来测试构建在 Insight 数据库之上的存储库 class...

[TestFixture]
public class CalculationResultsRepositoryTests
{
    private IFixture _fixture;

    private IDbConnection _connection;
    private CalculationResultsRepository _calculationResultsRepository;

    [SetUp]
    public void Setup()
    {
        _fixture = new Fixture().Customize(new AutoConfiguredNSubstituteCustomization());
        _connection = _fixture.Freeze<IDbConnection>();
        _calculationResultsRepository = _fixture.Create<CalculationResultsRepository>();
    }

    [Test]
    public void TestReturnsPagedCalculationResults()
    {
        //Arrange
        var financialYear = _fixture.Create<int>();
        var pagedResults = _fixture.Create<PagedResults<ColleagueCalculationResult>>();
        _connection.QueryAsync(Arg.Any<string>(), Arg.Any<object>(), Arg.Any<IQueryReader<PagedResults<ColleagueCalculationResult>>>()).Returns(pagedResults);

        //Act
        var result = _calculationResultsRepository.PagedListAsync(financialYear);

        //Assert
        Assert.IsInstanceOf<PagedResults<ColleagueCalculationResult>>(result);
    }
}

但是,当 运行 测试时我们看到以下异常:

System.Reflection.TargetInvocationException : Exception has been thrown by the target of an invocation. ----> NSubstitute.Exceptions.UnexpectedArgumentMatcherException : Argument matchers (Arg.Is, Arg.Any) should only be used in place of member arguments. Do not use in a Returns() statement or anywhere else outside of a member call. Correct use: sub.MyMethod(Arg.Any()).Returns("hi") Incorrect use: sub.MyMethod("hi").Returns(Arg.Any())

我们对如何解决这个问题有点不知所措,但猜测这似乎与 return 类型在这个特定的参数中被定义为泛型有关InsightDatabase 中 QueryAsync() 扩展方法的重载:

public static Task<T> QueryAsync<T>(this IDbConnection connection, string sql, object parameters, IQueryReader<T> returns, CommandType commandType = CommandType.StoredProcedure, CommandBehavior commandBehavior = CommandBehavior.Default, int? commandTimeout = default(int?), IDbTransaction transaction = null, CancellationToken? cancellationToken = default(CancellationToken?), object outputParameters = null);

有人知道如何成功模拟这个吗?

为了完整起见,我们尝试替换的方法调用是这样的:

var results = await _connection.QueryAsync("GetCalculationResults", new { FinancialYearId = financialYearId, PageNumber = pageNumber, PageSize = pageSize },
                Query.ReturnsSingle<PagedResults<ColleagueCalculationResult>>()
                    .ThenChildren(Some<ColleagueCalculationResult>.Records));

我根据您的测试做了一些修改。看看有没有帮助。

[Test]
public async Task TestReturnsPagedCalculationResults()
{
    //Arrange
    var financialYear = _fixture.Create<int>();
    var pagedResults = _fixture.Create<PagedResults<ColleagueCalculationResult>>();
    _connection.QueryAsync(null, null, null).ReturnsForAnyArgs(Task.FromResult(pagedResults));

    //Act
    var result = await _calculationResultsRepository.PagedListAsync(financialYear);

    //Assert
    Assert.IsInstanceOf<PagedResults<ColleagueCalculationResult>>(result);
}

这可能不是最好的方法,但由于您不能模拟扩展方法,而且我没有时间编写 Insight 的测试实现,这似乎是目前可以接受的解决方案...

已创建 IInsightDatabase 接口:

public interface IInsightDatabase
{
    Task<T> QueryAsync<T>(string sql, object parameters, IQueryReader<T> returns, CommandType commandType = CommandType.StoredProcedure, CommandBehavior commandBehavior = CommandBehavior.Default, int? commandTimeout = default(int?), IDbTransaction transaction = null, CancellationToken? cancellationToken = default(CancellationToken?), object outputParameters = null);
}

已创建 IInsightDatabase 的具体实现:

public class InsightDatabase : IInsightDatabase
{
    private readonly IDbConnection _connection;

    public InsightDatabase(IDbConnection connection)
    {
        _connection = connection;
    }

    public async Task<T> QueryAsync<T>(string sql, object parameters, IQueryReader<T> returns, CommandType commandType = CommandType.StoredProcedure, CommandBehavior commandBehavior = CommandBehavior.Default, int? commandTimeout = default(int?), IDbTransaction transaction = null, CancellationToken? cancellationToken = default(CancellationToken?), object outputParameters = null)
    {
        return await _connection.QueryAsync(sql, parameters, returns, commandType, commandBehavior, commandTimeout, transaction, cancellationToken, outputParameters);
    }
}

具体实现现在已注入存储库 class 允许通过模拟 IInsightDatabase 对其进行测试:

private IFixture _fixture;

private IInsightDatabase _insightDatabase;
private CalculationResultsRepository _calculationResultsRepository;

[SetUp]
public void Setup()
{
    _fixture = new Fixture().Customize(new AutoConfiguredNSubstituteCustomization());
    _insightDatabase = _fixture.Freeze<IInsightDatabase>();
    _calculationResultsRepository = _fixture.Create<CalculationResultsRepository>();
}

[Test]
public async Task PagedListAsync_ReturnsPagedResults()
{
    //Arrange
    var financialYearId = _fixture.Create<int>();
    var pagedResults = _fixture.Create<PagedResults<ColleagueCalculationResult>>();
    _insightDatabase.QueryAsync(Arg.Any<string>(), Arg.Any<object>(), Arg.Any<IQueryReader<PagedResults<ColleagueCalculationResult>>>()).Returns(pagedResults);

    //Act
    var result = await _calculationResultsRepository.PagedListAsync(financialYearId);

    //Assert
    result.Should().NotBeNull();
    result.Should().BeOfType<PagedResults<ColleagueCalculationResult>>();
    result.Should().Be(pagedResults);
}

太棒了!存储库 class 现在可以测试,并且 Insights 处理 IDbConnection、调用扩展方法和所有其他脏东西都很好地隐藏在一些东西中,虽然不可测试,但应该很难破解。