使用 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、调用扩展方法和所有其他脏东西都很好地隐藏在一些东西中,虽然不可测试,但应该很难破解。
我们正在使用 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、调用扩展方法和所有其他脏东西都很好地隐藏在一些东西中,虽然不可测试,但应该很难破解。