[英]How do I properly unit test a repository-pattern method, that has coupling to Entity Framework / DbContext?
[英]How do I unit test a repository that uses DbContext with NSubstitute?
我有一个解决方案,其中我有一个包含从现有数据库生成的 EF6 .edmx 文件的数据项目。 我将实体拆分为一个单独的 Entities 项目,并有一个引用它们的 Repositories 项目。
我添加了一个带有一些常用方法的 BaseRepository,并希望对其进行单元测试。 班级的顶部看起来像这样......
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;
}
//...
}
按照我在https://stackoverflow.com/a/21074664/706346找到的代码,我尝试了以下...
[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
其_dbSet
为空...
_dbSet = _ctx.Set<T>();
我想在设置模拟上下文时需要添加另一行,但我不确定是什么。 我认为上面的代码应该足以填充 DbSet。
任何人都能够解释我错过了什么或做错了什么?
好吧,在尝试按照我在问题中展示的方式让自己发疯之后,我遇到了专为该任务设计的Effort ,并遵循了本教程,这让我继续前进。 我对他的代码有一些问题,我将在下面解释。
简而言之,我所做的是......
*) 在测试项目中安装 Effort.EF6。 我一开始犯了一个错误,安装了 Effort(没有 EF6 位),但遇到了各种各样的问题。 如果您使用的是 EF6(或我认为是 EF5),请确保安装此版本。
*) 修改 MyModel.Context.tt 文件以包含一个额外的构造函数,该构造函数采用 DbConnection... public MyEntities(DbConnection connection) : base(connection, true) { }
*) 将连接字符串添加到测试项目的 App.Config 文件中。 我从数据项目中逐字复制了这个。
*) 向测试类添加了初始化方法以设置上下文...
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 与其他模拟不同,您不会告诉模拟框架针对特定参数返回什么。 相反,您必须将 Effort 视为一个真正的数据库,尽管它是内存中的临时数据库。 因此,我在初始化时设置了一个模拟患者列表,将它们添加到数据库中,然后才进行实际测试。
希望这可以帮助某人。 结果证明它比我最初尝试的方式要容易得多。
我创建了一个 NSubstitute 扩展来帮助对存储库层进行单元测试,您可以在GitHub DbContextMockForUnitTests上找到它。 您要引用的主要文件是DbContextMockForUnitTests/MockHelpers/MockExtension.cs (它在用于测试async
同一文件夹中有 3 个相关代码文件),将所有 4 个文件复制并粘贴到您的项目中。 你可以看到这个单元测试,它展示了如何使用它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);
}
免责声明 - 我是上述存储库的作者,但它部分基于使用您自己的测试替身进行测试(EF6 以上)
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.