[英]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.