【问题标题】:Unit test (NUnit, Nsubstitute) ASP Core Service with MongoDB使用 MongoDB 进行单元测试(NUnit、Nsubstitute)ASP 核心服务
【发布时间】:2019-03-17 03:07:55
【问题描述】:

我有一个调用 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 集合?

【问题讨论】:

  • 与其担心在这个阶段模拟库,不如创建一个class TestAutoDb : IAutoDb 实现,而不是让它由MongoDb 支持,而是让它使用List&lt;Auto&gt;,你可以控制你的测试。 (IMongoQueryable 可能会给您带来问题。如果您的测试不需要它,您可以不执行,或更改为IQueryable。)我还考虑使用内存中的 MongoDb 进行测试。
  • @DavidTchepak 不确定我是否关注,你能在答案中演示吗?
  • @Aeseir 在我看来,您的IAutoDb 在暴露IMongoQueryable&lt;Auto&gt; 时看起来像leaky abstraction

标签: c# .net-core nunit nsubstitute


【解决方案1】:

在我看来,您的IAutoDb 在暴露IMongoQueryable&lt;Auto&gt; 时看起来像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);
}

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

【讨论】:

    【解决方案2】:

    我认为Nkosi has a correct answer 演示了模拟库的使用。在关于问题的评论线程中,我被要求提供一个使用测试实现而不是模拟库的示例。所以在这里,注释线程的附带条件是 IMongoQueryable&lt;Auto&gt; 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);
    }
    

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

    Mocking 库会自动创建这些测试类,并且可以更轻松地更改每个测试的行为(例如,调用 Get 返回失败的任务以模拟网络错误或类似情况),但您可以也手动执行此操作。如果您对手工操作感到厌烦,那么现在是寻找模拟库以使其更容易的好时机。 :)

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

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

    【讨论】:

      猜你喜欢
      • 2018-10-20
      • 1970-01-01
      • 1970-01-01
      • 2021-12-03
      • 1970-01-01
      • 2019-04-17
      • 2013-04-01
      • 1970-01-01
      • 2018-05-23
      相关资源
      最近更新 更多