我正在尝试为某个场景编写一些单元测试,在这种情况下,我要在一个事务下将数据写入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
方法的同时,我还需要模拟ReplaceOneAsync
和InsertOneAsync
方法,以便我可以强制其中的任何一种方法在测试和验证中抛出异常session.AbortTransaction();
是否按预期工作(或至少被调用)。关于其他一些问题,我看到有人建议不要对 MongoDB 功能进行单元测试,但是在我的场景中,我需要测试数据库中是否没有不一致的数据。请提出建议。
我正在使用适用于.Net 2.7.0,c#,xUnit的MongoDB驱动程序。
答案 0 :(得分:0)
我认为您在错误的树下吠叫。
您需要的是集成测试,而不是单元测试。在您的位置,我会忘记“单元测试”这一部分,因为您根本没有办法对其进行单元测试。而是编写一个集成测试,使用一些无效的数据使其失败,然后查看会发生什么。
您可以继续前进,花时间模拟所有内容,但是最终您将无法获得任何价值,因为您需要检查实际的数据库以查看发生了什么,并且没有任何模拟结果可以为您提供帮助
因此,为此准备一些真实的请求,好像您有一个API,也许使用诸如Postman之类的东西对它进行一些真实的测试。