MongoDB 的单元测试(NUnit、Nsubstitute)ASP 核心服务

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

我有一个调用 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>();

        // 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
    }
}

如何创建一个 mockdb 集合,供 class 中的所有测试使用?

在我看来,你的 IAutoDb 在暴露 IMongoQueryable<Auto> 时看起来像 leaky abstraction

除此之外,确实不需要后备存储来测试服务。

参加第一次考试 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);
}

理想情况下,您想验证被测对象的预期行为。因此,您模拟了预期的行为,以便被测对象在执行测试时表现出预期的行为。

我认为 用于演示模拟库的使用。在关于这个问题的评论线程中,我被要求提供一个使用测试实现而不是模拟库的示例。所以在这里,评论线程中的附带条件是 IMongoQueryable<Auto> GetQueryable() 不适合与持久性无关的接口,因此我们可以删除它或用 IQueryable 或其他适配器替换它。

有很多方法可以做到这一点。我使用了支持列表(也可以使用由 id 键入的 dictionary/map)来实现 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);
}

我认为像这样手动实现测试 classes 是开始测试的好方法,而不是直接跳到模拟库。

模拟库自动创建这些测试 classes,并使更改每个测试的行为变得更容易一些(例如调用 Get return 失败模拟网络错误或类似的任务),但您也可以手动执行此操作。如果您厌倦了手动执行此操作,那么现在是查看模拟库以简化此操作的好时机。 :)

完全避免模拟库也有好处。明确实施测试 class 可以说更简单。团队不需要学习新的库,在多个测试和固定装置中重用它很方便(可能用它来测试更复杂的集成场景),甚至可以在应用程序本身中使用(例如:提供演示模式或类似模式)。

由于这个特定接口的性质(它的成员之间有隐含的合同:调用 create 然后获取该 id 应该 return 新创建的实例),我倾向于使用显式测试 class 在这种情况下,我可以确保遵守这些合同。对我来说,当我不关心那些合同时,嘲笑是最有用的。我只需要知道某个成员被调用,或者当另一个成员 return 出现特定结果时,我的 class 会以预期的方式运行。