Home > front end >  Fake IMongoQueryable with FakeItEasy
Fake IMongoQueryable with FakeItEasy

Time:09-03

I'm developing an API which communicates with MongoDB and I need to create some statistics from one collection. I have the following service:

public class BoxService : IBoxService
{
    private readonly IMongoCollection<Box> _boxCollection;

    public BoxService(IOptions<DbSettings> dbSettings, IMongoClient mongoClient)
    {
        var mongoDatabase = mongoClient.GetDatabase(dbSettings.Value.DatabaseName);
        _boxCollection = mongoDatabase.GetCollection<Box>(dbSettings.Value.BoxCollectionName);
    }

    public async Task<List<BoxStatisticsDto>> GetBoxNumberStatisticsAsync()
    {
        var boxNumberStatisticsList = new List<BoxStatisticsDto>();
        var results = await _boxCollection.AsQueryable()
            .Select(box => new { box.WarehouseId, Content = box.Content ?? string.Empty })
            .ToListAsync();

        // More calculations with the results list

        return boxNumberStatisticsList;
    }
}

And the following test:

public class BoxServiceTest
{
    private readonly IMongoCollection<Box> _boxCollection;
    private readonly List<Box> _boxes;
    private readonly IBoxService _boxService;

    public BoxServiceTest()
    {
        _boxCollection = A.Fake<IMongoCollection<Box>>();
        _boxes = new List<Box> {...};

        var mockOptions = A.Fake<IOptions<DbSettings>>();
        var mongoClient = A.Fake<IMongoClient>();
        var mongoDb = A.Fake<IMongoDatabase>();

        A.CallTo(() => mongoClient.GetDatabase(A<string>._, default)).Returns(mongoDb);
        A.CallTo(() => mongoDb.GetCollection<Box>(A<string>._, default)).Returns(_boxCollection);
        _boxService = new BoxService(mockOptions, mongoClient);
    }
}

This is working so far, the BoxService is created with the fake parameters and I can test other functionalities of the service (FindAll, FindById, Create, etc.) but how can I test the GetBoxNumberStatisticsAsync function? I can't fake the AsQueryable because it's an extension method.

CodePudding user response:

As you've noted, you can't fake an extension method. This question is asked every once in a while. For example, see Faking an Extension Method in a 3rd Party Library. There are a few approaches:

  1. if the static method is simple enough, to divine what it does and fake the non-static methods that it calls
  2. add a layer of indirection: wrap the call to the extension method in an interface that you can fake
  3. don't fake the database. Instead, replace it with some in-memory analogue, if one exists (I don't know what's available for Mongo)

CodePudding user response:

Here is what I ended up with. A base interface for all my services:

public interface IBaseService<T>
{
    //generic method definitions for all services: Findall, FindById, Create, Update
}

An abstract class to have a generic constructor:

public abstract class BaseService<T> : IBaseService<T>
{
    protected BaseService(IOptions<DbSettings> dbSettings, IMongoClient mongoClient, string collectionName)
    {
        var mongoDatabase = mongoClient.GetDatabase(dbSettings.Value.DatabaseName);
        Collection = mongoDatabase.GetCollection<T>(collectionName);
    }

    protected IMongoCollection<T> Collection { get; }

    // abstract method definitions for IBaseService stuff

    public virtual async Task<List<T>> CollectionToListAsync()
    {
        return await Collection.AsQueryable().ToListAsync();
    }
}

An interface for my BoxService:

public interface IBoxService : IBaseService<Box>
{
    public Task<List<BoxStatisticsDto>> GetBoxNumberStatisticsAsync();
}

The service itself:

public class BoxService : BaseService<Box>, IBoxService
{
    public BoxService(IOptions<DbSettings> dbSettings, IMongoClient mongoClient)
        : base(dbSettings, mongoClient, dbSettings.Value.BoxCollectionName)
    {
    }

    public async Task<List<BoxStatisticsDto>> GetBoxNumberStatisticsAsync()
    {
        var boxNumberStatisticsList = new List<BoxStatisticsDto>();
        var list = await CollectionToListAsync();
        var results = list.Select(box => new { box.WarehouseId, Content = box.Content ?? string.Empty }).ToList();

        //...

        return boxNumberStatisticsList;
    }
}

And finally the test:

public async Task GetBoxNumberStatisticsAsync_ReturnsStatistics()
{
    // Arrange
    var expected = new List<BoxStatisticsDto> {...};

    var fakeService = A.Fake<BoxService>(options => options.CallsBaseMethods());
    A.CallTo(() => fakeService.CollectionToListAsync()).Returns(_boxes);

    // Act
    var boxList = await ((IBoxService)fakeService).GetBoxNumberStatisticsAsync();
    
    // Assert
}

I'm not a huge fan of making the CollectionToListAsync public, but nothing really worked for me here. I tried creating IQueryable and IEnumerable from my list and convert them to IMongoQueryable but no success. I also tried faking the IMongoQueryable but I couldn't execute the Select on it as it gave an error that the 'collectionNamespace' can't be null and the CollectionNamespace can't be faked, because it's a sealed class.

  • Related