如何对使用 DbContext 和 NSubstitute 的存储库进行单元测试?
How do I unit test a repository that uses DbContext with NSubstitute?
我有一个解决方案,其中我有一个包含从现有数据库生成的 EF6 .edmx 文件的数据项目。我将实体拆分为一个单独的实体项目,并有一个引用它们的存储库项目。
我添加了一个带有一些常用方法的 BaseRepository,想对其进行单元测试。 class 的顶部看起来像这样...
public class BaseRepository<T> : BaseRepositoryInterface<T> where T : class {
private readonly MyEntities _ctx;
private readonly DbSet<T> _dbSet;
public BaseRepository(MyEntities ctx) {
_ctx = ctx;
_dbSet = _ctx.Set<T>();
}
public IEnumerable<T> GetAll() {
return _dbSet;
}
//...
}
根据我在 找到的代码,我尝试了以下...
[TestMethod]
public void BaseRepository_GetAll() {
IDbSet<Patient> mockDbSet = Substitute.For<IDbSet<Patient>>();
mockDbSet.Provider.Returns(GetPatients().Provider);
mockDbSet.Expression.Returns(GetPatients().Expression);
mockDbSet.ElementType.Returns(GetPatients().ElementType);
mockDbSet.GetEnumerator().Returns(GetPatients().GetEnumerator());
MyEntities mockContext = Substitute.For<MyEntities>();
mockContext.Patients.Returns(mockDbSet);
BaseRepositoryInterface<Patient> patientsRepository
= new BaseRepository<Patient>(mockContext);
List<Patient> patients = patientsRepository.GetAll().ToList();
Assert.AreEqual(GetPatients().Count(), patients.Count);
}
private IQueryable<Patient> GetPatients() {
return new List<Patient> {
new Patient {
ID = 1,
FirstName = "Fred",
Surname = "Ferret"
}
}
.AsQueryable();
}
请注意,我将上下文 TT 文件更改为使用 IDbSet,正如 Stuart Clement 在 2015 年 12 月 4 日 22:41
的评论中所建议的
但是,当我 运行 这个测试时,它给出了一个空引用异常,因为设置 _dbSet
的基本存储库构造函数中的行将其保留为空...
_dbSet = _ctx.Set<T>();
我想我在设置模拟上下文时需要添加另一行,但我不确定是什么。我认为上面的代码应该足以填充 DbSet。
谁能解释我错过了什么或做错了什么?
我创建了一个 NSubstitute 扩展来帮助对存储库层进行单元测试,您可以在 GitHub DbContextMockForUnitTests. The main file you want to reference is DbContextMockForUnitTests/MockHelpers/MockExtension.cs (it has 3 dependent code files in that same folder used for testing with async
) , copy and paste all 4 files into your project. You can see this unit test that shows how to use it DbContextMockForUnitTests/DbSetTests.cs 上找到它。
为了使它与您的代码相关,我们假设您已经复制了主文件并在 using
语句中引用了正确的命名空间。您的代码将是这样的(如果 MyEntities
未密封,您不需要更改它,但我仍然会作为编码的一般规则尝试接受尽可能不具体的类型):
// Slight change to BaseRepository, see comments
public class BaseRepository<T> : BaseRepositoryInterface<T> where T : class {
private readonly DbContext _ctx; // replaced with DbContext as there is no need to have a strong reference to MyEntities, keep it generic as possible unless there is a good reason not to
private readonly DbSet<T> _dbSet;
// replaced with DbContext as there is no need to have a strong reference to MyEntities, keep it generic as possible unless there is a good reason not to
public BaseRepository(DbContext ctx) {
_ctx = ctx;
_dbSet = _ctx.Set<T>();
}
public IEnumerable<T> GetAll() {
return _dbSet;
}
//...
}
单元测试代码:
// unit test
[TestMethod]
public void BaseRepository_GetAll() {
// arrange
// this is the mocked data contained in your mocked DbContext
var patients = new List<Patient>(){
new Patient(){/*set properties for mocked patient 1*/},
new Patient(){/*set properties for mocked patient 2*/},
new Patient(){/*set properties for mocked patient 3*/},
new Patient(){/*set properties for mocked patient 4*/},
/*and more if needed*/
};
// Create a fake/Mocked DbContext
var mockedContext = NSubstitute.Substitute.For<DbContext>();
// call to extension method which mocks the DbSet and adds it to the DbContext
mockedContext.AddToDbSet(patients);
// create your repository that you want to test and pass in the fake DbContext
var repo = new BaseRepository<Patient>(mockedContext);
// act
var results = repo.GetAll();
// assert
Assert.AreEqual(results.Count(), patients.Count);
}
免责声明 - 我是上述存储库的作者,但它部分基于 Testing with Your Own Test Doubles (EF6 onwards)
好吧,在试图按照我在问题中展示的方式去做这件事时,我把自己逼疯了,我遇到了 Effort, which was designed for the task, and followed this tutorial,这让我继续前进。我在他的代码中遇到了一些问题,我将在下面解释。
简而言之,我所做的是...
*) 在测试项目中安装 Effort.EF6。我一开始犯了一个错误,安装了 Effort(没有 EF6 位),并且遇到了各种各样的问题。如果您使用的是 EF6(我认为是 EF5),请确保安装此版本。
*) 修改了 MyModel.Context.tt 文件以包含一个采用 DbConnection 的额外构造函数... public MyEntities(DbConnection connection) : base(connection, true) { }
*) 将连接字符串添加到测试项目的 App.Config 文件中。我从数据项目中逐字复制了这个。
*) 在测试中添加了一个初始化方法 class 来设置上下文...
private MyEntities _ctx;
private BaseRepository<Patient> _patientsRepository;
private List<Patient> _patients;
[TestInitialize]
public void Initialize() {
string connStr = ConfigurationManager.ConnectionStrings["MyEntities"].ConnectionString;
DbConnection connection = EntityConnectionFactory.CreateTransient(connStr);
_ctx = new MyEntities(connection);
_patientsRepository = new BaseRepository<Patient>(_ctx);
_patients = GetPatients();
}
重要 - 在链接的文章中,他使用 DbConnectionFactory.CreateTransient()
,当我尝试 运行 测试时出现异常。这似乎是针对 Code First 的,因为我使用的是 Model First,所以我不得不将其更改为使用 EntityConnectionFactory.CreateTransient()
。
*)实际测试相当简单。我添加了一些辅助方法来尝试整理它,并使其更易于重用。在完成之前,我可能会再进行几轮重构,但这行得通,而且现在已经足够干净了...
[TestMethod]
public void BaseRepository_Update() {
AddAllPatients();
Assert.AreEqual(_patients.Count, _patientsRepository.GetAll().Count());
}
#region Helper methods
private List<Patient> GetPatients() {
return Enumerable.Range(1, 10).Select(CreatePatient).ToList();
}
private static Patient CreatePatient(int id) {
return new Patient {
ID = id,
FirstName = "FirstName_" + id,
Surname = "Surname_" + id,
Address1 = "Address1_" + id,
City = "City_" + id,
Postcode = "PC_" + id,
Telephone = "Telephone_" + id
};
}
private void AddAllPatients() {
_patients.ForEach(p => _patientsRepository.Update(p));
}
#endregion
这里需要转变思路的一点是,与其他模拟不同,使用 Effort 时,您不需要告诉模拟框架 return 特定参数的内容。相反,您必须将 Effort 视为一个真实的数据库,尽管它是内存中的一个临时数据库。因此,我在初始化时设置了一个模拟患者列表,将它们添加到数据库中,然后才进行实际测试。
希望这对某人有所帮助。事实证明,这比我最初尝试的方法要容易得多。
我有一个解决方案,其中我有一个包含从现有数据库生成的 EF6 .edmx 文件的数据项目。我将实体拆分为一个单独的实体项目,并有一个引用它们的存储库项目。
我添加了一个带有一些常用方法的 BaseRepository,想对其进行单元测试。 class 的顶部看起来像这样...
public class BaseRepository<T> : BaseRepositoryInterface<T> where T : class {
private readonly MyEntities _ctx;
private readonly DbSet<T> _dbSet;
public BaseRepository(MyEntities ctx) {
_ctx = ctx;
_dbSet = _ctx.Set<T>();
}
public IEnumerable<T> GetAll() {
return _dbSet;
}
//...
}
根据我在 找到的代码,我尝试了以下...
[TestMethod]
public void BaseRepository_GetAll() {
IDbSet<Patient> mockDbSet = Substitute.For<IDbSet<Patient>>();
mockDbSet.Provider.Returns(GetPatients().Provider);
mockDbSet.Expression.Returns(GetPatients().Expression);
mockDbSet.ElementType.Returns(GetPatients().ElementType);
mockDbSet.GetEnumerator().Returns(GetPatients().GetEnumerator());
MyEntities mockContext = Substitute.For<MyEntities>();
mockContext.Patients.Returns(mockDbSet);
BaseRepositoryInterface<Patient> patientsRepository
= new BaseRepository<Patient>(mockContext);
List<Patient> patients = patientsRepository.GetAll().ToList();
Assert.AreEqual(GetPatients().Count(), patients.Count);
}
private IQueryable<Patient> GetPatients() {
return new List<Patient> {
new Patient {
ID = 1,
FirstName = "Fred",
Surname = "Ferret"
}
}
.AsQueryable();
}
请注意,我将上下文 TT 文件更改为使用 IDbSet,正如 Stuart Clement 在 2015 年 12 月 4 日 22:41
的评论中所建议的但是,当我 运行 这个测试时,它给出了一个空引用异常,因为设置 _dbSet
的基本存储库构造函数中的行将其保留为空...
_dbSet = _ctx.Set<T>();
我想我在设置模拟上下文时需要添加另一行,但我不确定是什么。我认为上面的代码应该足以填充 DbSet。
谁能解释我错过了什么或做错了什么?
我创建了一个 NSubstitute 扩展来帮助对存储库层进行单元测试,您可以在 GitHub DbContextMockForUnitTests. The main file you want to reference is DbContextMockForUnitTests/MockHelpers/MockExtension.cs (it has 3 dependent code files in that same folder used for testing with async
) , copy and paste all 4 files into your project. You can see this unit test that shows how to use it DbContextMockForUnitTests/DbSetTests.cs 上找到它。
为了使它与您的代码相关,我们假设您已经复制了主文件并在 using
语句中引用了正确的命名空间。您的代码将是这样的(如果 MyEntities
未密封,您不需要更改它,但我仍然会作为编码的一般规则尝试接受尽可能不具体的类型):
// Slight change to BaseRepository, see comments
public class BaseRepository<T> : BaseRepositoryInterface<T> where T : class {
private readonly DbContext _ctx; // replaced with DbContext as there is no need to have a strong reference to MyEntities, keep it generic as possible unless there is a good reason not to
private readonly DbSet<T> _dbSet;
// replaced with DbContext as there is no need to have a strong reference to MyEntities, keep it generic as possible unless there is a good reason not to
public BaseRepository(DbContext ctx) {
_ctx = ctx;
_dbSet = _ctx.Set<T>();
}
public IEnumerable<T> GetAll() {
return _dbSet;
}
//...
}
单元测试代码:
// unit test
[TestMethod]
public void BaseRepository_GetAll() {
// arrange
// this is the mocked data contained in your mocked DbContext
var patients = new List<Patient>(){
new Patient(){/*set properties for mocked patient 1*/},
new Patient(){/*set properties for mocked patient 2*/},
new Patient(){/*set properties for mocked patient 3*/},
new Patient(){/*set properties for mocked patient 4*/},
/*and more if needed*/
};
// Create a fake/Mocked DbContext
var mockedContext = NSubstitute.Substitute.For<DbContext>();
// call to extension method which mocks the DbSet and adds it to the DbContext
mockedContext.AddToDbSet(patients);
// create your repository that you want to test and pass in the fake DbContext
var repo = new BaseRepository<Patient>(mockedContext);
// act
var results = repo.GetAll();
// assert
Assert.AreEqual(results.Count(), patients.Count);
}
免责声明 - 我是上述存储库的作者,但它部分基于 Testing with Your Own Test Doubles (EF6 onwards)
好吧,在试图按照我在问题中展示的方式去做这件事时,我把自己逼疯了,我遇到了 Effort, which was designed for the task, and followed this tutorial,这让我继续前进。我在他的代码中遇到了一些问题,我将在下面解释。
简而言之,我所做的是...
*) 在测试项目中安装 Effort.EF6。我一开始犯了一个错误,安装了 Effort(没有 EF6 位),并且遇到了各种各样的问题。如果您使用的是 EF6(我认为是 EF5),请确保安装此版本。
*) 修改了 MyModel.Context.tt 文件以包含一个采用 DbConnection 的额外构造函数... public MyEntities(DbConnection connection) : base(connection, true) { }
*) 将连接字符串添加到测试项目的 App.Config 文件中。我从数据项目中逐字复制了这个。
*) 在测试中添加了一个初始化方法 class 来设置上下文...
private MyEntities _ctx;
private BaseRepository<Patient> _patientsRepository;
private List<Patient> _patients;
[TestInitialize]
public void Initialize() {
string connStr = ConfigurationManager.ConnectionStrings["MyEntities"].ConnectionString;
DbConnection connection = EntityConnectionFactory.CreateTransient(connStr);
_ctx = new MyEntities(connection);
_patientsRepository = new BaseRepository<Patient>(_ctx);
_patients = GetPatients();
}
重要 - 在链接的文章中,他使用 DbConnectionFactory.CreateTransient()
,当我尝试 运行 测试时出现异常。这似乎是针对 Code First 的,因为我使用的是 Model First,所以我不得不将其更改为使用 EntityConnectionFactory.CreateTransient()
。
*)实际测试相当简单。我添加了一些辅助方法来尝试整理它,并使其更易于重用。在完成之前,我可能会再进行几轮重构,但这行得通,而且现在已经足够干净了...
[TestMethod]
public void BaseRepository_Update() {
AddAllPatients();
Assert.AreEqual(_patients.Count, _patientsRepository.GetAll().Count());
}
#region Helper methods
private List<Patient> GetPatients() {
return Enumerable.Range(1, 10).Select(CreatePatient).ToList();
}
private static Patient CreatePatient(int id) {
return new Patient {
ID = id,
FirstName = "FirstName_" + id,
Surname = "Surname_" + id,
Address1 = "Address1_" + id,
City = "City_" + id,
Postcode = "PC_" + id,
Telephone = "Telephone_" + id
};
}
private void AddAllPatients() {
_patients.ForEach(p => _patientsRepository.Update(p));
}
#endregion
这里需要转变思路的一点是,与其他模拟不同,使用 Effort 时,您不需要告诉模拟框架 return 特定参数的内容。相反,您必须将 Effort 视为一个真实的数据库,尽管它是内存中的一个临时数据库。因此,我在初始化时设置了一个模拟患者列表,将它们添加到数据库中,然后才进行实际测试。
希望这对某人有所帮助。事实证明,这比我最初尝试的方法要容易得多。