Модульное тестирование (Nunit, Nsubstitute) ASP Core Service с MongoDb - PullRequest
0 голосов
/ 17 марта 2019

У меня есть простое приложение, которое вызывает коллекцию mongodb, и делает с ней разные вещи.Я хочу провести модульное тестирование моего сервисного уровня с помощью Nunit, Nsubstitute, но не знаю, как смоделировать сбор данных, который использует мой сервисный слой.

Вот мои текущие настройки:

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();
    }

Мой уровень обслуживания

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);

}

Моя попытка модульного тестирования уровня обслуживания:

public class AutoServiceTests
{
    private IAutoDb _AutoDb;

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

        // don't know how to mock a dataset that contains 3 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
    }
}

Любые советы о том, как создать коллекцию mockdb, которая может бытьбудет оценен по всем тестам в классе.

Ответы [ 2 ]

1 голос
/ 17 марта 2019

На мой взгляд, ваша IAutoDb выглядит как негерметичная абстракция , когда она выставляет IMongoQueryable<Auto>.

Помимо этого, на самом деле нет необходимости в резервном хранилище для того, чтобыпротестируйте сервис.

Пройдите свой первый тест CreateAuto.Его поведение может быть подтверждено путем соответствующей настройки макета

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);
}

Для двух других тестов в предметной службе не было никаких реализаций, отражающих то, что нужно было проверить, поэтому я создал несколько образцов

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();
    }
}

, чтобы продемонстрировать простой способ их проверки.

[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);
}

В идеале вы хотите проверить ожидаемое поведение испытуемого объекта.Поэтому вы высмеиваете ожидаемое поведение, чтобы испытуемый вел себя так, как ожидалось при выполнении тестов.

0 голосов
/ 19 марта 2019

Я думаю, что @Nkosi имеет правильный ответ для демонстрации использования библиотеки для насмешек.В ветке комментариев по этому вопросу меня попросили привести пример использования тестовой реализации, а не библиотеки-насмешки.Итак, вот с оговоркой из ветки комментариев, что IMongoQueryable<Auto> GetQueryable() не подходит для интерфейса, не зависящего от постоянства, и поэтому мы можем удалить его или заменить его на IQueryable или другой адаптер.

ТамЕсть много способов сделать это.Я использовал список поддержки (мог также использовать словарь / карту с ключом по идентификатору) для реализации версии в памяти IAutoDb: (Отказ от ответственности: черновик. Пожалуйста, просмотрите и протестируйте, прежде чем использовать это где-либо)

class TestAutoDb : IAutoDb
{
    public List<Auto> Autos = new List<Auto>();
    public Task<Auto> Create(Auto auto) {
        Autos.Add(auto);
        return Task.FromResult(auto);
    }
    public Task<Auto> Get(Guid id) => Task.Run(() => Autos.Find(x => x.Id == id));
    public Task<List<Auto>> GetAll() => Task.FromResult(Autos);
    public Task Remove(Auto model) => Task.Run(() => Autos.Remove(model));
    public Task Remove(Guid id) => Task.Run(() => Autos.RemoveAll(x => x.Id == id));
    public Task Update(Guid id, Auto model) => Remove(id).ContinueWith(_ => Create(model));
}

Теперь мы можем тестировать по известным состояниям находящейся в памяти базы данных:

[Fact]
public async Task Get3Autos() {
    var db = new TestAutoDb();
    // Add 3 autos
    var firstGuid = new Guid(1, 2, 3, new byte[] { 4, 5, 6, 7, 8, 9, 10, 11 });
    db.Autos = new List<Auto> {
        new Auto { Id = firstGuid, Name = "Abc" },
        new Auto { Id = Guid.NewGuid(), Name = "Def" },
        new Auto { Id = Guid.NewGuid(), Name = "Ghi" }
    };
    var service = new AutoService(db);

    // Check service layer (note: just delegates to IAutoDb, so not a very useful test)
    var result = await service.GetAutoById(firstGuid);

    Assert.Equal(db.Autos[0], result);
}

Я думаю, что ручная реализация таких тестовых классов - хороший способ начатьс тестированием, а не прыгать прямо в издевательскую библиотекуБиблиотеки-насмешки автоматизируют создание этих классов тестов и немного упрощают изменение поведения для каждого теста (например, вызов Get возвращает невыполненную задачу для имитации сетевой ошибки или аналогичной), но вы также можете сделать этовручную.Если вам скучно делать это вручную, то сейчас самое время обратиться к библиотеке для насмешек, чтобы сделать это проще.:)

Есть также преимущества, позволяющие полностью избежать насмешливых библиотек.Возможно, проще иметь явно реализованный тестовый класс, команде не нужно изучать новую библиотеку, удобно повторно использовать ее в нескольких тестах и ​​приспособлениях (возможно, используя ее также для тестирования более сложных сценариев интеграции), и можетдаже потенциально может использоваться в самом приложении (например: для предоставления демонстрационного режима или аналогичного).

Из-за природы этого конкретного интерфейса (его участники имеют подразумеваемые контракты между ними: вызов create и затем получение этогоid должен возвращать только что созданный экземпляр). В этом случае я бы склонялся к использованию явного тестового класса, чтобы гарантировать соблюдение этих контрактов.Для меня насмешка наиболее полезна, когда меня не волнуют эти контракты, мне просто нужно знать, что был вызван определенный член, или что, когда другой член возвращает определенный результат, мой класс действует ожидаемым образом.

...