skip to Main Content

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.

2

Answers


  1. Chosen as BEST ANSWER

    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.


  2. 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)
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search