MongoDB 的单元测试(NUnit、Nsubstitute)ASP 核心服务
Unit test (NUnit, Nsubstitute) ASP Core Service with MongoDB
我有一个调用 MongoDB 集合的简单应用程序,它用它做各种事情。
我想使用 NUnit、Nsubstitute 对我的服务层进行单元测试,但我不知道如何模拟我的服务层使用的数据集合。
这是我当前的设置:
AutoDB:
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 中的所有测试使用?
在我看来,你的 IAutoDb
在暴露 IMongoQueryable<Auto>
时看起来像 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);
}
理想情况下,您想验证被测对象的预期行为。因此,您模拟了预期的行为,以便被测对象在执行测试时表现出预期的行为。
我认为 用于演示模拟库的使用。在关于这个问题的评论线程中,我被要求提供一个使用测试实现而不是模拟库的示例。所以在这里,评论线程中的附带条件是 IMongoQueryable<Auto> GetQueryable()
不适合与持久性无关的接口,因此我们可以删除它或用 IQueryable
或其他适配器替换它。
有很多方法可以做到这一点。我使用了支持列表(也可以使用由 id 键入的 dictionary/map)来实现 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);
}
我认为像这样手动实现测试 classes 是开始测试的好方法,而不是直接跳到模拟库。
模拟库自动创建这些测试 classes,并使更改每个测试的行为变得更容易一些(例如调用 Get
return 失败模拟网络错误或类似的任务),但您也可以手动执行此操作。如果您厌倦了手动执行此操作,那么现在是查看模拟库以简化此操作的好时机。 :)
完全避免模拟库也有好处。明确实施测试 class 可以说更简单。团队不需要学习新的库,在多个测试和固定装置中重用它很方便(可能用它来测试更复杂的集成场景),甚至可以在应用程序本身中使用(例如:提供演示模式或类似模式)。
由于这个特定接口的性质(它的成员之间有隐含的合同:调用 create 然后获取该 id 应该 return 新创建的实例),我倾向于使用显式测试 class 在这种情况下,我可以确保遵守这些合同。对我来说,当我不关心那些合同时,嘲笑是最有用的。我只需要知道某个成员被调用,或者当另一个成员 return 出现特定结果时,我的 class 会以预期的方式运行。
我有一个调用 MongoDB 集合的简单应用程序,它用它做各种事情。
我想使用 NUnit、Nsubstitute 对我的服务层进行单元测试,但我不知道如何模拟我的服务层使用的数据集合。
这是我当前的设置:
AutoDB:
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 中的所有测试使用?
在我看来,你的 IAutoDb
在暴露 IMongoQueryable<Auto>
时看起来像 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);
}
理想情况下,您想验证被测对象的预期行为。因此,您模拟了预期的行为,以便被测对象在执行测试时表现出预期的行为。
我认为 IMongoQueryable<Auto> GetQueryable()
不适合与持久性无关的接口,因此我们可以删除它或用 IQueryable
或其他适配器替换它。
有很多方法可以做到这一点。我使用了支持列表(也可以使用由 id 键入的 dictionary/map)来实现 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);
}
我认为像这样手动实现测试 classes 是开始测试的好方法,而不是直接跳到模拟库。
模拟库自动创建这些测试 classes,并使更改每个测试的行为变得更容易一些(例如调用 Get
return 失败模拟网络错误或类似的任务),但您也可以手动执行此操作。如果您厌倦了手动执行此操作,那么现在是查看模拟库以简化此操作的好时机。 :)
完全避免模拟库也有好处。明确实施测试 class 可以说更简单。团队不需要学习新的库,在多个测试和固定装置中重用它很方便(可能用它来测试更复杂的集成场景),甚至可以在应用程序本身中使用(例如:提供演示模式或类似模式)。
由于这个特定接口的性质(它的成员之间有隐含的合同:调用 create 然后获取该 id 应该 return 新创建的实例),我倾向于使用显式测试 class 在这种情况下,我可以确保遵守这些合同。对我来说,当我不关心那些合同时,嘲笑是最有用的。我只需要知道某个成员被调用,或者当另一个成员 return 出现特定结果时,我的 class 会以预期的方式运行。