繁体   English   中英

使用 MongoDB 进行单元测试(NUnit、Nsubstitute)ASP 核心服务

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

我有一个调用 MongoDB 集合的简单应用程序,它可以用它做各种事情。

我想使用 NUnit、Nsubstitute 对我的服务层进行单元测试,但我不知道如何模拟我的服务层使用的数据集合。

这是我目前的设置:

自动数据库:

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 集合?

在我看来,您的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);
}

理想情况下,您希望验证被测对象的预期行为。 因此,您模拟预期的行为,以便在执行测试时被测对象的行为符合预期。

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

有很多方法可以做到这一点。 我使用了一个支持列表(也可以使用以 id 为键的字典/地图)来实现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 应该返回新创建的实例),在这种情况下,我倾向于使用显式测试类,这样我就可以确保这些契约被遵守。 对我来说,当我不关心这些合同时,嘲笑是最有用的。 我只需要知道某个成员被调用,或者当另一个成员返回特定结果时,我的班级以预期的方式行事。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM