MockQueryable

repository·master·Indexed 21 days ago

https://github.com/ramantsitou/mockqueryable

A lightweight testing library for mocking IQueryable<T> and Entity Framework Core asynchronous query operations without a real database. It provides support for Moq, NSubstitute, and FakeItEasy, enabling the emulation of async EF Core extensions like ToListAsync and AnyAsync against in-memory collections. Features include the ability to mock DbSet<T>, implement custom FindAsync logic, and use custom ExpressionVisitors to rewrite provider-specific expressions such as EF.Functions.Like.

Tokens
1.8K
Snippets
7
Records
10
Agent score
25%

What's inside MockQueryable

  1. How MockQueryable works

    master

    MockQueryable enables unit testing of services that depend on Entity Framework Core by allowing IQueryable<T> to execute asynchronous EF Core extensions (like ToListAsync(), FirstOrDefaultAsync(), AnyAsync(), etc.) against in-memory collections.

    The Execution Pipeline

    1. Data Source: An in-memory collection (e.g., List<T>).
    2. Mock Building: The collection is converted into a mock using BuildMock() or BuildMockDbSet().
    3. Async Emulation: The library implements IAsyncEnumerable<T>, IAsyncQueryProvider, and IAsyncEnumerator<T> to bridge the gap between standard LINQ and EF Core's async APIs.
    4. Expression Processing: LINQ expressions are captured as expression trees.
    5. Expression Rewriting: An optional ExpressionVisitor can intercept and rewrite provider-specific expressions (like EF.Functions.Like) into standard LINQ expressions (like .Contains()) that can run in memory.
    6. Execution: The rewritten expression is compiled and executed against the in-memory data.
  2. Install MockQueryable via NuGet

    master

    MockQueryable provides several NuGet packages depending on your target framework and mocking library. Choose the package that matches your project requirements:

    • Core functionality: MockQueryable.Core
    • EF Core support: MockQueryable.EntityFrameworkCore
    • Moq integration: MockQueryable.Moq
    • NSubstitute integration: MockQueryable.NSubstitute
    • FakeItEasy integration: MockQueryable.FakeItEasy
    # Install via Package Manager
    Install-Package MockQueryable.Core
    Install-Package MockQueryable.EntityFrameworkCore
    Install-Package MockQueryable.Moq
    Install-Package MockQueryable.NSubstitute
    Install-Package MockQueryable.FakeItEasy
  3. Extend MockQueryable with a Custom Expression Visitor

    master

    You can emulate database-specific behavior (like EF.Functions.Like) by providing a custom ExpressionVisitor. This allows you to rewrite complex provider-specific expressions into standard LINQ expressions that the in-memory engine can execute.

    1. Define a class inheriting from ExpressionVisitor.
    2. Use the generic BuildMock<T, TVisitor>() method to attach it to your mock.

    Example of rewriting EF.Functions.Like to .Contains():

    public class SampleLikeExpressionVisitor : ExpressionVisitor
    {
        // Implementation to rewrite Like expressions to Contains
    }
    
    // Usage
    var mock = users.BuildMock<UserEntity, SampleLikeExpressionVisitor>();
    public class SampleLikeExpressionVisitor : ExpressionVisitor
    {
    }
    
    // Usage
    var mock = users.BuildMock<UserEntity, SampleLikeExpressionVisitor>();
  4. How to mock DbSet<T> with MockQueryable

    master

    If your code interacts directly with a DbSet<T>, use the .BuildMockDbSet<T>() extension method. This returns a mock object that can be used with different frameworks:

    • For Moq, use the .Object property of the returned mock.
    • For NSubstitute or FakeItEasy, use the returned object directly.
    var mockDbSet = users.BuildMockDbSet();
    
    // Moq
    var repo = new TestDbSetRepository(mockDbSet.Object);
    
    // NSubstitute / FakeItEasy
    var repo = new TestDbSetRepository(mockDbSet);
  5. How to mock IQueryable with MockQueryable

    master

    To mock an IQueryable<T> for testing async EF Core extensions (like ToListAsync, AnyAsync, or FirstOrDefaultAsync), follow these three steps:

    1. Create your in-memory test data using a standard List<T>.
    2. Build the mock using the .BuildMock() extension method.
    3. Inject the mock into your repository or service using your preferred mocking framework.

    This allows you to test code that uses async LINQ queries without requiring a real database connection.

    // 1. Create Test Data
    var users = new List<UserEntity>
    {
        new UserEntity { LastName = "Smith", DateOfBirth = new DateTime(2012, 1, 20) },
        // More test data...
    };
    
    // 2. Build the Mock
    var mock = users.BuildMock(); // returns an IQueryable<T>
    
    // 3. Set Up in Your favorite Mocking Framework
    
    // Moq
    _userRepository.Setup(x => x.GetQueryable()).Returns(mock);
    
    // NSubstitute
    _userRepository.GetQueryable().Returns(mock);
    
    // FakeItEasy
    A.CallTo(() => userRepository.GetQueryable()).Returns(mock);
  6. Recommended Testing Strategy

    master

    MockQueryable is optimized for speed and isolation, but it does not execute actual SQL. Use the following strategy to balance speed and fidelity:

    Test TypeRecommended Tool
    Domain LogicMockQueryable
    Service LayerMockQueryable
    Repository LogicReal Provider
    SQL TranslationReal Provider
    MigrationsReal Database
    End-to-End TestsReal Database
  7. Mocking IQueryable with FakeItEasy

    master

    To use MockQueryable with FakeItEasy, include the MockQueryable.FakeItEasy package. Use BuildMock() to create the mock object.

    var mock = users.BuildMock();
    
    A.CallTo(() => repository.GetQueryable())
        .Returns(mock);
    A.CallTo(() => repository.GetQueryable())
        .Returns(mock);
  8. Mocking IQueryable with Moq

    master

    To use MockQueryable with the Moq framework, include the MockQueryable.Moq package. This provides extension methods to convert a list into a mock object that can be returned by a repository setup.

    var users = TestData.Users(); // Your in-memory data
    
    var mock = users.BuildMock();
    
    repository
        .Setup(x => x.GetQueryable())
        .Returns(mock);
    var users = TestData.Users();
    
    var mock = users.BuildMock();
    
    repository
        .Setup(x => x.GetQueryable())
        .Returns(mock);
  9. Add custom logic to a MockQueryable mock

    master

    You can extend the behavior of a mock to handle specific method calls that aren't covered by standard LINQ providers, such as FindAsync.

    Custom FindAsync

    Use the .Setup() method on the mock to define custom logic for FindAsync. The argument passed to the returned function will be the array of IDs passed to FindAsync.

    Custom Expression Visitor

    You can build a mock with a custom ExpressionVisitor to support specific EF functions (like EF.Functions.Like) by passing the visitor type as a generic argument to BuildMockDbSet.

    // Custom FindAsync
    mock.Setup(x => x.FindAsync(userId)).ReturnsAsync((object[] ids) =>
    {
        var id = (Guid)ids[0];
        return users.FirstOrDefault(x => x.Id == id);
    });
    
    // Custom Expression Visitor
    var mockDbSet = users.BuildMockDbSet<UserEntity, SampleLikeExpressionVisitor>();