1

I work on a project which doesn't have an integration tests setup.

When I work on some backend task I need to have some test coverage for the stuff which is related to the interaction with the database.

I use EF Core 3.1, and because it already implements a repository pattern, I'm able to create extension methods per different entities.

And here the thing appears. Each LINQ query is translated into pure SQL. This also means that the logic of the query must behave exactly the same as translated SQL code.

I've checked and I'm allowed to write unit tests against non-async methods however in the case of the Async methods it doesn't seem to be that trivial.

For example, I have the following extension method:

public static class PostcodeExclusionExtension
{

    public static bool CheckPostcodeIsSupported(this IQueryable<PostcodeExclusion> postcodeExclusion, int customerId, string postcode) =>
        !postcodeExclusion.Any(ApplyIsSupportedExpression(customerId, postcode));

    public static async Task<bool> CheckPostcodeIsSupportedAsync(this IQueryable<PostcodeExclusion> postcodeExclusion, int customerId, string postcode) =>
        !await postcodeExclusion.AnyAsync(ApplyIsSupportedExpression(customerId, postcode));

    private static Expression<Func<PostcodeExclusion, bool>> ApplyIsSupportedExpression(int customerId, string postcode)
    {
        postcode = postcode.Replace(" ", "").ToUpper();
        postcode = postcode.Insert(postcode.Length - 3, " ");
        var postcodeMatches = Enumerable.Range(0, postcode.Length)
                                    .Select(x => postcode.Substring(0, x + 1))
                                    .ToArray();

        return x => x.CustomerID == customerId && postcodeMatches.Contains(x.Postcode);
    }
}

This is unit tests coverage(written in xUnit):

public static int validCustomerId = 1;
public const string validPostcode = "SW1A0AA";

public static IEnumerable<object[]> CheckPostcodeExclusion_should_validate_postcode_Inputs = new List<object[]>
{
    new object[] { true, validPostcode, validCustomerId, new List<PostcodeExclusion> { new PostcodeExclusion() { CustomerID = validCustomerId, Postcode = "A" } } },
    new object[] { true, validPostcode, validCustomerId, new List<PostcodeExclusion> { new PostcodeExclusion() { CustomerID = validCustomerId, Postcode = "SX" } } },
    //other test cases...    
};

[Theory]
[MemberData(nameof(CheckPostcodeExclusion_should_validate_postcode_Inputs))]
public async Task CheckPostcodeExclusion_should_validate_postcode(bool expectedPostcodeIsSupported, string postcode, int customerId, IEnumerable<PostcodeExclusion> postcodeExclusionSet)
{
    var isSupported = await postcodeExclusionSet.AsQueryable().CheckPostcodeIsSupportedAsync(customerId, postcode);
    Assert.Equal(expectedPostcodeIsSupported, isSupported);
}

When I run the test against the Async method I'm getting

The provider for the source IQueryable doesn't implement IAsyncQueryProvider. Only providers that implement IAsyncQueryProvider can be used for Entity Framework asynchronous operations.

I found this workaround however it only works for the EF core 2.2. I tried to somehow implement a similar idea for EF core 3.1 but without a result. At the moment I covered the non-async method with tests however I use Async one in the production. Better having something than nothing... Any ideas? Cheers

Erik Philips
  • 53,428
  • 11
  • 128
  • 150
GoldenAge
  • 2,918
  • 5
  • 25
  • 63
  • 1
    Could you please elaborate on this: *I tried to somehow implement a similar idea for EF core 3.1 but without a result.* ? – Peter Csala Jul 22 '21 at 14:39
  • @PeterCsala yea so I tried to modify the code provided in [this answer](https://stackoverflow.com/questions/51023223/the-provider-for-the-source-iqueryable-doesnt-implement-iasyncqueryprovider/51025518#51025518) to make it work for EF Core 3.1 but without a positive result.. – GoldenAge Jul 22 '21 at 15:27
  • 1
    Did you try In-memory provider? If your tests don't relay on navigation properties it can be very convenient. https://learn.microsoft.com/en-us/ef/core/testing/in-memory – Artur Jul 23 '21 at 15:15
  • @GoldenAge [Gary McGill](https://stackoverflow.com/users/98422/gary-mcgill) in the comments section had left a [link](https://stackoverflow.com/questions/57314896/iasyncqueryprovider-mock-issue-when-migrated-to-net-core-3-adding-tresult-iasyn/58314109#58314109) for 3.x. Did you check that as well? – Peter Csala Jul 26 '21 at 09:43
  • @GoldenAge What, specifically, didn't work about that solution? Just saying it doesn't work isn't helpful. Provide the details about what happened, and how that differs from what you expect to happen. = – Servy Jul 26 '21 at 18:38
  • Just a friendly reminder to add CancellationToken support for all Async methods :) – Isitar Jul 27 '21 at 11:59

2 Answers2

1

If you create an extension method called AnyAsyncOrSync() which checks whether the source provider is capable of asynchronous enumeration before attempting it...

public static async Task<bool> AnyAsyncOrSync<TSource>([NotNull] this IQueryable<TSource> source, [NotNull] Expression<Func<TSource, bool>> predicate, CancellationToken cancellationToken = default)
{
    return source is IAsyncEnumerable<TSource>
        ? await source.AnyAsync(predicate, cancellationToken)
        : source.Any(predicate);
}

...you can catch the problem before it gets into the framework...

!await postcodeExclusion.AnyAsyncOrSync(ApplyIsSupportedExpression(customerId, postcode));
Creyke
  • 1,887
  • 2
  • 12
  • 16
-1

It doesn't make sense to return IQueryable async. It doesn't return data, just a query stub.

Return IEnumerableAsync or wrap it in a Task.Run( () => YourMethod())

Peter Csala
  • 17,736
  • 16
  • 35
  • 75
m0r6aN
  • 850
  • 1
  • 11
  • 19