Search code examples
c#.net-corenunitnsubstitute

Unit test (NUnit, Nsubstitute) ASP Core Service with MongoDB


I got a simple application that calls a MongoDB collection, and it does various things with it.

I want to unit test my service layer using NUnit, Nsubstitute, but I have no idea how to mock a data collection that my service layer consumes.

Here is my current setup:

AutoDB:

public class AutoDb : IAutoDb
{
    private readonly IMongoCollection<Auto> _AutosCollection;

    public AutoDb(IConfiguration config)
    {
        var client = new MongoClient(config.GetConnectionString("DatabaseConnection"));
        var database = client.GetDatabase("AutoDb");

        _AutosCollection = database.GetCollection<Auto>("Autos");

        var AutoKey = Builders<Auto>.IndexKeys;
        var indexModel = new CreateIndexModel<Auto>(AutoKey.Ascending(x => x.Email), new CreateIndexOptions {Unique = true});

        _AutosCollection.Indexes.CreateOne(indexModel);
    }

    public async Task<List<Auto>> GetAll()
    {
        return await _AutosCollection.Find(_ => true).ToListAsync();
    }

    public async Task<Auto> Get(Guid id)
    {
        return await _AutosCollection.Find<Auto>(o => o.Id == id).FirstOrDefaultAsync();
    }

    public async Task<Auto> Create(Auto Auto)
    {
        await _AutosCollection.InsertOneAsync(Auto);
        return Auto;
    }

    public async Task Update(Guid id, Auto model)
    {
        await _AutosCollection.ReplaceOneAsync(o => o.Id == id, model);
    }

    public async Task Remove(Auto model)
    {
        await _AutosCollection.DeleteOneAsync(o => o.Id == model.Id);
    }

    public async Task Remove(Guid id)
    {
        await _AutosCollection.DeleteOneAsync(o => o.Id == id);
    }

    public IMongoQueryable<Auto> GetQueryable() => _AutosCollection.AsQueryable();
}

public interface IAutoDb
{
    Task<List<Auto>> GetAll();

    Task<Auto> Get(Guid id);

    Task<Auto> Create(Auto Auto);

    Task Update(Guid id, Auto model);

    Task Remove(Auto model);

    Task Remove(Guid id);

    IMongoQueryable<Auto> GetQueryable();
}

My Service Layer

public class AutoService : IAutoService
{
    private readonly IAutoDb _AutoDb;

    public AutoService(IAutoDb AutoDb)
    {
        _AutoDb = AutoDb;
    }

    public async Task<Auto> CreateProfile(AutoModel model)
    {

        var Auto = new Auto
        {
            Id = new Guid(),
            Type = model.Type,
            Name = model.Name,
        };

        try
        {
            await _AutoDb.Create(Auto);

        }
        catch (MongoWriteException mwx)
        {
            Debug.WriteLine(mwx.Message);
            return null;
        }

        return Auto;
    }

    public async Task<Auto> GetAutoById(Guid id)
    {
        var retVal = await _AutoDb.Get(id);

        return retVal;
    }

    public Task<Auto> EditAuto(AutoModel model)
    {
        throw new NotImplementedException();
    }
}

public interface IAutoService
{
    Task<Auto> CreateProfile(AutoModel model);
    Task<Auto> EditAuto(AutoModel model);
    Task<Auto> GetAutoById(Guid id);

}

My attempt at unit testing the service layer:

public class AutoServiceTests
{
    private IAutoDb _AutoDb;

    [SetUp]
    public void Setup()
    {
        _AutoDb = Substitute.For<IAutoDb>();

        // I don't know how to mock a dataset that contains
        // three auto entities that can be used in all tests
    }

    [Test]
    public async Task CreateAuto()
    {
        var service = new AutoService(_AutoDb);

        var retVal = await service.CreateProfile(new AutoModel
        {
            Id = new Guid(),
            Type = "Porsche",
            Name = "911 Turbo",
        });

        Assert.IsTrue(retVal is Auto);
    }

    [Test]
    public async Task Get3Autos() {
        var service = new AutoService(_AutoDb);

        // Stopped as I don't have data in the mock db
    }

    [Test]
    public async Task Delete1AutoById() {
        var service = new AutoService(_AutoDb);

        // Stopped as I don't have data in the mock db
    }
}

How can I create a mockdb collection that can be consumed by all the tests in the class?


Solution

  • In my opinion, your IAutoDb looks like a leaky abstraction when it exposes IMongoQueryable<Auto>.

    Aside from that, there really is no need for a backing store in order to test the service.

    Take your first test CreateAuto. Its behavior can be asserted by configuring the mock accordingly:

    public async Task CreateAuto() {
    
        // Arrange
        var db = Substitute.For<IAutoDb>();
    
        // Configure mock to return the passed argument
        db.Create(Arg.Any<Auto>()).Returns(_ => _.Arg<Auto>());
    
        var service = new AutoService(db);
        var model = new AutoModel {
            Id = new Guid(),
            Type = "Porsche",
            Name = "911 Turbo",
        };
    
        // Act
        var actual = await service.CreateProfile(model);
    
        // Assert
        Assert.IsTrue(actual is Auto);
    }
    

    For the other two tests, there were not any implementations in the subject service to reflect what needed to be tested, so I created some samples,

    public interface IAutoService {
    
        // ...others omitted for brevity
    
        Task RemoveById(Guid id);
        Task<List<Auto>> GetAutos();
    }
    
    public class AutoService : IAutoService {
        private readonly IAutoDb _AutoDb;
    
        public AutoService(IAutoDb AutoDb) {
            _AutoDb = AutoDb;
        }
    
        // ...others omitted for brevity
    
        public Task RemoveById(Guid id) {
            return _AutoDb.Remove(id);
        }
    
        public Task<List<Auto>> GetAutos() {
            return _AutoDb.GetAll();
        }
    }
    

    in order to demonstrate a simple way to test them.

    [Test]
    public async Task Get3Autos() {
        var db = Substitute.For<IAutoDb>();
        var expected = new List<Auto>() {
            new Auto(),
            new Auto(),
            new Auto(),
        };
        db.GetAll().Returns(expected);
    
        var service = new AutoService(db);
    
        // Act
        var actual = await service.GetAutos();
    
        // Assert
        CollectionAssert.AreEqual(expected, actual);
    }
    
    [Test]
    public async Task Delete1AutoById() {
    
        // Arrange
        var expectedId = Guid.Parse("FF28A47B-9A87-4184-919A-FDBD414D0AB5");
        Guid actualId = Guid.Empty;
        var db = Substitute.For<IAutoDb>();
        db.Remove(Arg.Any<Guid>()).Returns(_ => {
            actualId = _.Arg<Guid>();
            return Task.CompletedTask;
        });
    
        var service = new AutoService(db);
    
        // Act
        await service.RemoveById(expectedId);
    
        // Assert
        Assert.AreEqual(expectedId, actualId);
    }
    

    Ideally you want to verify the expected behavior of the subject under test. Therefore you mock the expected behavior so that the subject under test behaves as expected when the tests are exercised.