如何模拟MongoDb集合以在C#中返回一些虚拟数据?

时间:2019-05-11 22:50:55

标签: c# mongodb unit-testing xunit.net

我正在尝试为某个场景编写一些单元测试,在这种情况下,我要在一个事务下将数据写入2个不同的集合中。该功能在手动测试期间可以正常工作(例如,在ReplaceOneAsync块内的try...catch方法调用之后强制抛出异常)。我想测试一下,如果其中一个写入操作失败,那么整个事务都会回滚。下面是我的示例代码:

我的DbContext:

public class DbContext : IDbContext
{
    private readonly IMongoDatabase _database;
    // construcor code to populate _database
    // code for other collections
    public IMongoDatabase Database => _database;

    public IMongoCollection<CustomerDoc> Customers => _database.GetCollection<CustomerDoc>("Customers");
    public IMongoCollection<BusinessLogDoc> BusinessLogs => _database.GetCollection<BusinessLogDoc>("BusinessLogs");
}

我的数据库操作方法:

public async Task<GetCustomerResponse> UpSertCustomerAsync(UpSertCustomerRequest request)
{
    // some other code ...
    var existing = await _dbContext.Customers.Find(p => p.Name == request.Name).FirstOrDefaultAsync();
    if (existing != null && existing.Id != request.Id)
    {
        throw new Exception($"Name:{request.Name} already used.");
    }
    // code to populate customer and businessLog object
    // ...
    var session = await _dbContext.Database.Client.StartSessionAsync();
    session.StartTransaction();

    try
    {
        await _dbContext.Customers.ReplaceOneAsync(session, doc => doc.Id == request.Id, customer, new UpdateOptions { IsUpsert = true });
        await _dbContext.BusinessLogs.InsertOneAsync(session, businessLog);
        session.CommitTransaction();
    }
    catch (Exception ex)
    {
        session.AbortTransaction();
        throw new Exception("Database operation failed, rolling back.");
    }
    // code to return response
}   

您注意到上面的代码中,我首先检查客户是否已经存在。现在下面是我的单元测试代码:

单元测试

public interface IFakeMongoCollection : IMongoCollection<BsonDocument>
{
    IFindFluent<BsonDocument, BsonDocument> Find(FilterDefinition<BsonDocument> filter, FindOptions options);
    IFindFluent<BsonDocument, BsonDocument> Project(ProjectionDefinition<BsonDocument, BsonDocument> projection);
    IFindFluent<BsonDocument, BsonDocument> Skip(int skip);
    IFindFluent<BsonDocument, BsonDocument> Limit(int limit);
    IFindFluent<BsonDocument, BsonDocument> Sort(SortDefinition<BsonDocument> sort);
}

public class MyControllerTests
{
    private IOptions<MongoClientSettings> _mongoSettings;
    private Mock<IFakeMongoCollection> _fakeMongoCollection;
    private Mock<IMongoDatabase> _fakeMongoDatabase;
    private Mock<IMyDbContext> _fakeMongoContext;
    private Mock<IFindFluent<BsonDocument, BsonDocument>> _fakeCollectionResult;

    private Mock<IMongoCollection<CustomerDoc>> _customers;
    private Mock<IMongoCollection<BusinessLogDoc>> _logs;

    private MyController _controller;

    public MyControllerTests()
    {
        _fakeMongoCollection = new Mock<IFakeMongoCollection>();
        _fakeCollectionResult = new Mock<IFindFluent<BsonDocument, BsonDocument>>();
        _fakeMongoDatabase = new Mock<IMongoDatabase>();

        _customers = new Mock<IMongoCollection<CustomerDoc>>();
        _logs = new Mock<IMongoCollection<BusinessLogDoc>>();
    }

    private void SetupMockCollection(string collectionName)
    {
        _customers.Object.InsertOne(new CustomerDoc
        {
            Id = ObjectId.GenerateNewId()
        });

        switch (collectionName)
        {
            case "Customers":
                _fakeMongoDatabase.Setup(_ => _.GetCollection<CustomerDoc>(collectionName, It.IsAny<MongoCollectionSettings>()))
                    .Returns(_customers.Object);
                break;
            case "BusinessLogs":
                _fakeMongoDatabase.Setup(_ => _.GetCollection<BusinessLogDoc>(collectionName, It.IsAny<MongoCollectionSettings>())).Returns(_logs.Object);
                break;
            default:
                _fakeMongoDatabase.Setup(_ => _.GetCollection<BsonDocument>(collectionName, It.IsAny<MongoCollectionSettings>()))
                    .Returns(_fakeMongoCollection.Object);
                break;
        }

        _fakeMongoContext = new Mock<IMyDbContext>();
        _fakeMongoContext.Setup(t => t.Database).Returns(_fakeMongoDatabase.Object);
        _fakeMongoContext.Setup(t => t.BusinessLogs).Returns(_logs.Object);
    }

    [Fact]
    public void Test()
    {
        SetupMockCollection("Customers");

        var context = _fakeMongoContext.Object;
        var db = _fakeMongoDatabase.Object;

        var collection = db.GetCollection<CustomerDoc>("Customers");

        // how to create an object of type IMongoCollection<CustomerDoc> 
        // how to mock collection.Find<CustomerDoc> so that it returns the dummy data

        // below line returns mocked object without the data I have added inside SetupMockCollection method
        var docs = collection.Find<CustomerDoc>(t => t.Id != null);

        int count = 0;
        try
        {
            count = docs.ToList().Count;//throws exception
        }
        catch (Exception)
        {
            count = 0;
        }
        Assert.NotNull(docs);
        Assert.Equal(1, count);
    }
}  

在模拟Find方法的同时,我还需要模拟ReplaceOneAsyncInsertOneAsync方法,以便我可以强制其中的任何一种方法在测试和验证中抛出异常session.AbortTransaction();是否按预期工作(或至少被调用)。关于其他一些问题,我看到有人建议不要对 MongoDB 功能进行单元测试,但是在我的场景中,我需要测试数据库中是否没有不一致的数据。请提出建议。

我正在使用适用于.Net 2.7.0,c#,xUnit的MongoDB驱动程序。

1 个答案:

答案 0 :(得分:0)

我认为您在错误的树下吠叫。

您需要的是集成测试,而不是单元测试。在您的位置,我会忘记“单元测试”这一部分,因为您根本没有办法对其进行单元测试。而是编写一个集成测试,使用一些无效的数据使其失败,然后查看会发生什么。

您可以继续前进,花时间模拟所有内容,但是最终您将无法获得任何价值,因为您需要检查实际的数据库以查看发生了什么,并且没有任何模拟结果可以为您提供帮助

因此,为此准备一些真实的请求,好像您有一个API,也许使用诸如Postman之类的东西对它进行一些真实的测试。