最小起订量 - 使用 DbEntityEntry 更新
Moq - Update with DbEntityEntry
我正在使用 EF6。生成的代码类似于:
public partial class MyDataContext : DbContext
{
public MyDataContext() : base("name=mydata")
{
}
public virtual DbSet<Book> Books { get; set; }
}
然后我有一个通用存储库,如:
public class GenericRepository<TObject> where TObject : class
{
protected readonly MyDataContext Context;
protected GenericRepository(MyDataContext context)
{
Context = context;
}
public virtual TObject Update(TObject data, int id)
{
if (data == null)
return null;
TObject obj = Context.Set<TObject>().Find(id);
if (obj != null)
{
Context.Entry(obj).CurrentValues.SetValues(data);
Context.SaveChanges();
}
return obj;
}
}
然后我有一个使用 GenericRepository 更新数据的服务:
public class MyDataService<TObject> where TObject : class
{
private readonly MyDataContext context;
public MyDataService(MyDataContext ct)
{
context = ct;
}
public TObject Update(TObject obj, int id)
{
var r = new GenericRepository<TObject>(context);
return r.Update(obj, id);
}
}
所以我可以用这样的东西更新一本书:
var ds = new MyDataService<Book>(new MyDataContext());
var data = ds.Update(new Book { Name = "New Name" }, 1);
这工作正常。接下来我尝试使用 Moq 对上面的代码进行单元测试,例如:
var updatedBook = new Book { Name = "Update Book Name" };
var mockSet = new Mock<DbSet<Book>>();
var mockContext = new Mock<MyDataContext>();
mockContext.Setup(c => c.Books).Returns(mockSet.Object);
mockContext.Setup(c => c.Set<Book>().Find(It.IsAny<object[]>()))
.Returns<object[]>(ids => chips.FirstOrDefault(d => d.Id == (int)ids[0]));
var service = new MyDataService<Book>(mockContext.Object);
var data = service.Update(updatedBook, 1);
但是,我在 Context.Entry(obj).CurrentValues.SetValues(data)
行遇到异常。
如何正确模拟 Update 方法?
您可以为 MyDataService
实现一个接口,以便能够模拟它
public Interface IMyDataService<TObject> where TObject : class
{
TObject Update(TObject obj, int id);
}
public class MyDataService<TObject>:IMyDataService<TObject>
where TObject : class
{
private readonly MyDataContext context;
public MyDataService(MyDataContext ct)
{
context = ct;
}
public TObject Update(TObject obj, int id)
{
var r = new GenericRepository<TObject>(context);
return r.Update(obj, id);
}
}
起订量:
var mockDataService = new Mock<IMyDataService<Book>>();
mockDataService.Setup(c=> c.Update(It.Any<Book>(),It.Any<int>()).Returns(updatedbook);
我建议进行小的重构,以使测试更容易甚至成为可能。使用此实现,您依赖于 DbContext 和 DbEntityEntry 的实现。
首先为您的上下文提取界面:
public inteface IMyDataContext<TObject> where TObject is class
{
TObject FindById(int id); //call FindId
void Update(TObject); //call DbEntityEntry SetValues
void SaveChanges();
}
然后在GenericRepository中注入接口。这将使您的生活更轻松,然后您可以轻松地模拟所有方法。存储库的单元测试应验证是否调用了正确的上下文方法。
该服务应该依赖于存储库。将上下文直接传递给服务会产生误导,因为服务真正需要和使用的是存储库。
您的 classes 应该依赖于抽象而不是具体化。也就是说,以上所有 classes 都可以在接口后面抽象出来。但现在我将专注于服务 class 以及它对存储库的依赖。您将不同的层耦合得太紧密了。服务层不需要知道数据上下文
抽象存储库以允许更容易测试
interface IGenericRepository<TObject> where TObject : class {
TObject Update(TObject data, int id);
}
public class GenericRepository<TObject> : IGenericRepository<TObject> where TObject : class {
protected readonly MyDataContext Context;
public GenericRepository(MyDataContext context) {
Context = context;
}
public virtual TObject Update(TObject data, int id) {
if (data == null)
return null;
TObject obj = Context.Set<TObject>().Find(id);
if (obj != null) {
Context.Entry(obj).CurrentValues.SetValues(data);
Context.SaveChanges();
}
return obj;
}
}
服务现在只需要知道存储库抽象,而不是其实现细节。
public class MyDataService<TObject> where TObject : class {
private readonly IGenericRepository<TObject> repository;
public MyDataService(IGenericRepository<TObject> repository) {
this.repository = repository;
}
public TObject Update(TObject obj, int id) {
return repository.Update(obj, id);
}
}
现在可以单独测试服务,无需担心任何数据上下文
//Arrange
var updatedBook = new Book { Name = "Update Book Name" };
var id = 1;
var mockRepository = new Mock<IGenericRepository<Book>>();
mockRepository
.Setup(m => m.Update(updatedBook, id))
.Returns(updatedBook);
var service = new MyDataService<Book>(mockRepository.Object);
//Act
var data = service.Update(updatedBook, id);
//Assert
//...
当需要单独对存储库实现进行单元测试时,您可以遵循相同的结构并抽象存储库实现的上下文。
我正在使用 EF6。生成的代码类似于:
public partial class MyDataContext : DbContext
{
public MyDataContext() : base("name=mydata")
{
}
public virtual DbSet<Book> Books { get; set; }
}
然后我有一个通用存储库,如:
public class GenericRepository<TObject> where TObject : class
{
protected readonly MyDataContext Context;
protected GenericRepository(MyDataContext context)
{
Context = context;
}
public virtual TObject Update(TObject data, int id)
{
if (data == null)
return null;
TObject obj = Context.Set<TObject>().Find(id);
if (obj != null)
{
Context.Entry(obj).CurrentValues.SetValues(data);
Context.SaveChanges();
}
return obj;
}
}
然后我有一个使用 GenericRepository 更新数据的服务:
public class MyDataService<TObject> where TObject : class
{
private readonly MyDataContext context;
public MyDataService(MyDataContext ct)
{
context = ct;
}
public TObject Update(TObject obj, int id)
{
var r = new GenericRepository<TObject>(context);
return r.Update(obj, id);
}
}
所以我可以用这样的东西更新一本书:
var ds = new MyDataService<Book>(new MyDataContext());
var data = ds.Update(new Book { Name = "New Name" }, 1);
这工作正常。接下来我尝试使用 Moq 对上面的代码进行单元测试,例如:
var updatedBook = new Book { Name = "Update Book Name" };
var mockSet = new Mock<DbSet<Book>>();
var mockContext = new Mock<MyDataContext>();
mockContext.Setup(c => c.Books).Returns(mockSet.Object);
mockContext.Setup(c => c.Set<Book>().Find(It.IsAny<object[]>()))
.Returns<object[]>(ids => chips.FirstOrDefault(d => d.Id == (int)ids[0]));
var service = new MyDataService<Book>(mockContext.Object);
var data = service.Update(updatedBook, 1);
但是,我在 Context.Entry(obj).CurrentValues.SetValues(data)
行遇到异常。
如何正确模拟 Update 方法?
您可以为 MyDataService
实现一个接口,以便能够模拟它
public Interface IMyDataService<TObject> where TObject : class
{
TObject Update(TObject obj, int id);
}
public class MyDataService<TObject>:IMyDataService<TObject>
where TObject : class
{
private readonly MyDataContext context;
public MyDataService(MyDataContext ct)
{
context = ct;
}
public TObject Update(TObject obj, int id)
{
var r = new GenericRepository<TObject>(context);
return r.Update(obj, id);
}
}
起订量:
var mockDataService = new Mock<IMyDataService<Book>>();
mockDataService.Setup(c=> c.Update(It.Any<Book>(),It.Any<int>()).Returns(updatedbook);
我建议进行小的重构,以使测试更容易甚至成为可能。使用此实现,您依赖于 DbContext 和 DbEntityEntry 的实现。
首先为您的上下文提取界面:
public inteface IMyDataContext<TObject> where TObject is class
{
TObject FindById(int id); //call FindId
void Update(TObject); //call DbEntityEntry SetValues
void SaveChanges();
}
然后在GenericRepository中注入接口。这将使您的生活更轻松,然后您可以轻松地模拟所有方法。存储库的单元测试应验证是否调用了正确的上下文方法。
该服务应该依赖于存储库。将上下文直接传递给服务会产生误导,因为服务真正需要和使用的是存储库。
您的 classes 应该依赖于抽象而不是具体化。也就是说,以上所有 classes 都可以在接口后面抽象出来。但现在我将专注于服务 class 以及它对存储库的依赖。您将不同的层耦合得太紧密了。服务层不需要知道数据上下文
抽象存储库以允许更容易测试
interface IGenericRepository<TObject> where TObject : class {
TObject Update(TObject data, int id);
}
public class GenericRepository<TObject> : IGenericRepository<TObject> where TObject : class {
protected readonly MyDataContext Context;
public GenericRepository(MyDataContext context) {
Context = context;
}
public virtual TObject Update(TObject data, int id) {
if (data == null)
return null;
TObject obj = Context.Set<TObject>().Find(id);
if (obj != null) {
Context.Entry(obj).CurrentValues.SetValues(data);
Context.SaveChanges();
}
return obj;
}
}
服务现在只需要知道存储库抽象,而不是其实现细节。
public class MyDataService<TObject> where TObject : class {
private readonly IGenericRepository<TObject> repository;
public MyDataService(IGenericRepository<TObject> repository) {
this.repository = repository;
}
public TObject Update(TObject obj, int id) {
return repository.Update(obj, id);
}
}
现在可以单独测试服务,无需担心任何数据上下文
//Arrange
var updatedBook = new Book { Name = "Update Book Name" };
var id = 1;
var mockRepository = new Mock<IGenericRepository<Book>>();
mockRepository
.Setup(m => m.Update(updatedBook, id))
.Returns(updatedBook);
var service = new MyDataService<Book>(mockRepository.Object);
//Act
var data = service.Update(updatedBook, id);
//Assert
//...
当需要单独对存储库实现进行单元测试时,您可以遵循相同的结构并抽象存储库实现的上下文。