0

We're using Nunit, NSubstitute and AutoFixture to test a repository class that's built on top of Insight database...

[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);
    }
}

However, when running the test we're seeing the following exception:

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())

We're at a bit of a loss with how to resolve this, however at a guess it seems to be something to do with the return type being defined as a generic within a parameter on this particular overload of the QueryAsync() extension method within InsightDatabase:

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);

Does anybody know how to successfully mock this?

For completeness the method call we're trying to substitute is this:

var results = await _connection.QueryAsync("GetCalculationResults", new { FinancialYearId = financialYearId, PageNumber = pageNumber, PageSize = pageSize },
                Query.ReturnsSingle<PagedResults<ColleagueCalculationResult>>()
                    .ThenChildren(Some<ColleagueCalculationResult>.Records));
Ruben Bartelink
  • 59,778
  • 26
  • 187
  • 249
James Law
  • 6,067
  • 4
  • 36
  • 49

2 Answers2

2

This probably isn't the best approach but as you can't mock extension methods and I don't have the time to write a test implementation of Insight this seems to be an acceptable solution for now...

Created IInsightDatabase interface:

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);
}

Created concrete implementation of 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);
    }
}

The concrete implementation is now injected into the repository class allowing that to be tested by mocking 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);
}

Tah-dah! The repository class is now testable and Insights dealings with IDbConnection, calls to extension methods and all other nastiness is nicely tucked away in something that, although not testable, should be fairly difficult to break.

James Law
  • 6,067
  • 4
  • 36
  • 49
1

I did a few changes based on your test. See if it helps.

[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);
}
Stephen Zeng
  • 2,748
  • 21
  • 18
  • Hi Stephen, thanks for the suggestion but sadly it doesn't work. Result Message: NSubstitute.Exceptions.CouldNotSetReturnDueToTypeMismatchException : Can not return value of type Task`1 for IDbConnection.Open (expected type Void). Make sure you called Returns() after calling your substitute (for example: mySub.SomeMethod().Returns(value)), and that you are not configuring other substitutes within Returns() (for example, avoid this: mySub.SomeMethod().Returns(ConfigOtherSub())). – James Law Aug 17 '16 at 09:34
  • From the looks of the error, including the Insight.Database namespace to have available the QueryAsync namespace attempts to call Open on the connection. So in theory if we remove the reference and then mock the extension method it might work - any idea how to do this? – James Law Aug 17 '16 at 09:43
  • What is the implementation of the extension method? – Stephen Zeng Aug 17 '16 at 09:47
  • Also the key thing here is to make sure your repository instance calls the mocked connection instance. Just by looking at your code, it doesn't look like it's the case. What is your repository implementation? – Stephen Zeng Aug 17 '16 at 09:51
  • The QueryAsync extension method signature is in the original question. The IDbConnection is frozen before the repository fixture is created therefore the IDbConnection is automatically injected by AutoFixture. – James Law Aug 17 '16 at 09:55
  • Seems either the repository or the extension method called connection.open(). Try mock open() as well serif it works. (Without seeing the code I can only guess) – Stephen Zeng Aug 17 '16 at 09:58
  • I just said that about 10 minutes ago dude! – James Law Aug 17 '16 at 09:59
  • No that's different. The namespace shouldn't matter. You cannot mock extension method as well. What I would do is find out where the open gets called. – Stephen Zeng Aug 17 '16 at 10:03
  • It's the same thing. The extension method is defined in the insight.database namespace, including it causes the extension method to be called which in turn calls the open method, which is a void. – James Law Aug 17 '16 at 10:06
  • Will check the details once I get home. – Stephen Zeng Aug 17 '16 at 10:09
  • Checked the insight database extension class source code, the open gets called in it. And you repository calls _connection.QueryAsync(). You can write a test extension method to work around it but that means you need to change the code of your repository, which defeats the purpose of the test. The right way is to use a mocked Insight.Database if there is such framework. Also your test's assert does not make much sense. The returned object's type is always the method defined return type, doesn' matter the logic inside, as long as the call is successful. You should assert something else. – Stephen Zeng Aug 17 '16 at 10:27
  • Agreed - one of my shithead colleagues wrote the assert. Thanks for the advice. I think I'm going to abstract the QueryAsync method into another interface that's then injected into the repo, then the concrete implementation can call into the IDbConnection - it's not perfect as it's not 100% code coverage but it'll at least mean the repo classes are tested. – James Law Aug 17 '16 at 10:31