Daniel Edholm Ignat
Daniel Edholm Ignat

Reputation: 268

Unit testing MongoDB.Driver dotnet core

We are using a Command/Query pattern where the implementation has detailed knowledge about how MongoDB works and we want to write a test for that. Mocking MongoDb IMongoCollection<CarDocument> while also making sure the correct Find filter is sent is quite challenging. We are using .NET core 2.1 and MongoDB.Driver v2.7.2

using MongoDB.Driver;

namespace Example
{


    public class SomeMongoThing : ISomeMongoThing
    {
        public IMongoCollection<CarDocument> GetCars()
        {
            var client = new MongoClient("ConnectionString");
            var database = client.GetDatabase("DatabaseName");
            return database.GetCollection<CarDocument>("CollectionName");
        }
    }

    public interface ISomeMongoThing
    {
        IMongoCollection<CarDocument> GetCars();
    }

    public class GetCarQuery
    {
        private readonly ISomeMongoThing someMongoThing;

        public GetCarQuery(ISomeMongoThing someMongoThing)
        {
            this.someMongoThing = someMongoThing;
        }

        public CarDocument Query(string aKey)
        {
            var schedules = someMongoThing.GetCars();

            var match = schedules.Find(x => x.AKey == aKey);
            return match.Any() ? match.First() : this.GetDefaultCar(schedules);
        }

        private CarDocument GetDefaultCar(IMongoCollection<CarDocument> schedules)
        {
            return schedules.Find(x => x.AKey == "Default").First();
        }
    }
}

We have a test here but we are unable to write a test which checks that the correct aKey-filter has been used, meaning if we use the filter x => x.AKey == "hello" in the implementation the test should fail. Even if the code has .Find(x => true) the tests pass.

using System.Collections.Generic;
using System.Threading;
using MongoDB.Driver;
using Moq;
using NUnit.Framework;

namespace Example
{
    public class GetCarQueryTest
    {
        [Test]
        public void ShouldGetByApiKey()
        {
            var mockCarDocument = new CarDocument();
            var aKey = "a-key";

            var result = Mock.Of<IAsyncCursor<CarDocument>>(x =>
                x.MoveNext(It.IsAny<CancellationToken>()) == true
                && x.Current == new List<CarDocument>() { mockCarDocument });
            var cars = Mock.Of<IMongoCollection<CarDocument>>(x => x.FindSync(
                It.IsAny<FilterDefinition<CarDocument>>(),
                It.IsAny<FindOptions<CarDocument, CarDocument>>(),
                It.IsAny<CancellationToken>()) == result);

            var someMongoThing = Mock.Of<ISomeMongoThing>(x => x.GetCars()() == cars);
            var getCarQuery = new GetCarQuery(someMongoThing);

            var car = getCarQuery.Query(aKey);

            car.Should().Be(mockCarDocument);
        }
    }
}

How would you test the provided code? If making the abstraction between SomeMongoThing and GetCarQuery helps things we are open to suggestions. The idea is that the Query has knowledge about MongoDb to be able to leverage the power of the MongoDb client, and that the user of the query does not have to care.

Upvotes: 3

Views: 4285

Answers (1)

Nkosi
Nkosi

Reputation: 247333

This, in my opinion appears to be a leaky abstraction as part of an XY problem.

From comments

No matter how you abstract some part of your code will handle MongoCollection, how do you test that class?

I wouldn't test that class. The class that ultimately wraps the collection would not need to be unit tested as it would simply be a wrapper for a 3rd party concern. The developers of MongoCollection would have tested their code for release. I see the whole mongo dependency as a 3rd party implementation concern.

Take a look at the following alternative design

public interface ICarRepository {
    IEnumerable<CarDocument> GetCars(Expression<Func<CarDocument, bool>> filter = null);
}

public class CarRepository : ICarRepository {
    private readonly IMongoDatabase database;

    public CarRepository(Options options) {
        var client = new MongoClient(options.ConnectionString);
        database = client.GetDatabase(options.DatabaseName);
    }

    public IEnumerable<CarDocument> GetCars(Expression<Func<CarDocument, bool>> filter = null) {
        IMongoCollection<CarDocument> cars = database.GetCollection<CarDocument>(options.CollectionName);
        return filter == null ? cars.AsQueryable() : (IEnumerable<CarDocument>)cars.Find(filter).ToList();
    }
}

For simplicity I've renamed some of the dependencies. It should be self explanatory. All Mongo related concerns are encapsulated within its own concern. The repository can leverage all the power of the MongoDb client as needed without leaking unnecessary dependencies on 3rd party concerns.

The dependent query class can be refactored accordingly

public class GetCarQuery {
    private readonly ICarRepository repository;

    public GetCarQuery(ICarRepository repository) {
        this.repository = repository;
    }

    public CarDocument Query(string aKey) {
        var match = repository.GetCars(x => x.AKey == aKey);
        return match.Any()
            ? match.First()
            : repository.GetCars(x => x.AKey == "Default").FirstOrDefault();
    }
}

A happy path for the above class can now be simply mocked in an isolated unit test

public class GetCarQueryTest {
    [Test]
    public void ShouldGetByApiKey() {
        //Arrange
        var aKey = "a-key";
        var mockCarDocument = new CarDocument() {
            AKey = aKey
        };

        var data = new List<CarDocument>() { mockCarDocument };

        var repository = new Mock<ICarRepository>();

        repository.Setup(_ => _.GetCars(It.IsAny<Expression<Func<CarDocument, bool>>>()))
            .Returns((Expression<Func<CarDocument, bool>> filter) => {
                return filter == null ? data : data.Where(filter.Compile());
            });

        var getCarQuery = new GetCarQuery(repository.Object);

        //Act
        var car = getCarQuery.Query(aKey);

        //Assert
        car.Should().Be(mockCarDocument);
    }
}

Testing actual Mongo related concerns would require an integration test where you would connect to an actual source to ensure expected behavior.

Upvotes: 2

Related Questions