Я пытаюсь написать несколько модульных тестов для сценария, в котором я пишу данные в 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();
работает, как ожидалось (или, по крайней мере, получил вызов). На некоторые другие вопросы SO, я видел, что люди предлагают НЕ тестировать функции MongoDB , но в моем сценарии мне нужно проверить, что в базе данных нет противоречивых данных. Пожалуйста, предложите.
Я использую драйвер MongoDB для .Net 2.7.0, c #, xUnit.