在控制器内部调用时,模拟的 DbSet 方法抛出 NotImplementedException
Mocked DbSet method throws NotImplementedException when called inside of controller
我有以下设置:
DbContext:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public virtual DbSet<Album> Album { get; set; }
public ApplicationDbContext()
: base("DefaultConnection", throwIfV1Schema: false)
{
}
public static ApplicationDbContext Create()
{
return new ApplicationDbContext();
}
}
型号:
public class Album
{
public int AlbumID { get; set; }
[StringLength(150)]
public string Title { get; set; }
}
控制器:
public class AlbumController : Controller
{
ApplicationDbContext db = new ApplicationDbContext();
public AlbumController(ApplicationDbContext injectDb)
{
db = injectDb;
}
// POST: Albums/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
[Authorize(Roles = "Admin")]
public ActionResult DeleteConfirmed(int id)
{
Album album = db.Album.Find(id);
db.Album.Remove(album);
db.SaveChanges();
return RedirectToAction("Index");
}
}
我使用 Moq 和 xUnit 编写了单元测试来检查 DeleteConfirmed 功能:
public class AlbumsControllerTests
{
public static Mock<DbSet<T>> MockDbSet<T>(List<T> inputDbSetContent) where T : class
{
var DbSetContent = inputDbSetContent.AsQueryable();
var dbSet = new Mock<DbSet<T>>();
dbSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(DbSetContent.Provider);
dbSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(DbSetContent.Expression);
dbSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(DbSetContent.ElementType);
dbSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(() => inputDbSetContent.GetEnumerator());
dbSet.Setup(m => m.Add(It.IsAny<T>())).Callback<T>((s) => inputDbSetContent.Add(s));
dbSet.Setup(m => m.Remove(It.IsAny<T>())).Callback<T>((s) => inputDbSetContent.Remove(s));
return dbSet;
}
[Fact]
public void DeleteConfirmedTest()
{
// Arrange
var mockAlbumSet = MockDbSet(new List<Album> { });
Mock<ApplicationDbContext> sutDbContext = new Mock<ApplicationDbContext>() { CallBase = true };
sutDbContext.Setup(m => m.Album).Returns(mockAlbumSet.Object);
// Check if Album.Remove works inside this test
var albumToBeDeleted = new Album() { AlbumID = 1, Title = "TestAlbumName" };
sutDbContext.Object.Album.Add(albumToBeDeleted);
Assert.Equal(1, (from a in sutDbContext.Object.Album select a).Count());
sutDbContext.Object.Album.Remove(albumToBeDeleted);
Assert.Equal(0, (from a in sutDbContext.Object.Album select a).Count());
// Actual Test
sutDbContext.Object.Album.Add(albumToBeDeleted);
sutDbContext.Setup(m => m.Album.Find(It.IsAny<int>()))
.Returns(albumToBeDeleted);
AlbumController sut = new AlbumController(sutDbContext.Object);
var output = sut.DeleteConfirmed(1); // Throws NotImplementedException
// Assert
Assert.Equal(0, (from a in sutDbContext.Object.Album select a).Count());
}
}
测试在 db.Album.Remove(album) in DeleteConfirmed 中抛出以下异常:
System.NotImplementedException : The member 'Remove' has not been
implemented on type 'DbSet1Proxy' which inherits from 'DbSet
1'. Test
doubles for 'DbSet`1' must provide implementations of methods and
properties that are used.
正如您在 MockDbSet 方法主体中看到的那样,我为我的 Mock 设置了 Remove 方法,它在单元测试中工作得很好。你能解释一下为什么它在控制器内部不起作用吗?
如果您更改线路,您的测试将正常运行:
sutDbContext.Setup(m => m.Album.Find(It.IsAny<int>()))
.Returns(albumToBeDeleted);
收件人:
mockAlbumSet.Setup(x=>x.Find(It.IsAny<int>()))
.Returns(albumToBeDeleted);
当 sutDbContext.Album
被调用时,您将 sutDbContext 设置为 return mockAlbumSet.Object
,但是该行覆盖了您的设置,为 sutDbContext.Album
创建了一个新的模拟对象属性 并为该模拟创建了一个设置:
m.Album.Find(It.IsAny<int>()))
.Returns(albumToBeDeleted);
这是一个简单的测试,向您展示调用 class 的嵌套 属性 的设置,之前设置为 return a Mock.Object,将用新的 Mock.Object:
覆盖 属性
public interface IParentService
{
IDependantService Dependant { get; }
}
public interface IDependantService
{
void Execute();
}
[Fact]
//This test passes
public void VerifyThatNestedMockSetupGeneratesNewMockObject()
{
var value = 0;
var parentServiceMock = new Mock<IParentService>();
var dependantServiceMock = new Mock<IDependantService>();
dependantServiceMock.Setup(x => x.Execute()).Callback(() => { value = 1; });
parentServiceMock.Setup(x => x.Dependant).Returns(dependantServiceMock.Object);
Assert.Same(parentServiceMock.Object.Dependant, dependantServiceMock.Object);
parentServiceMock.Setup(x => x.Dependant.Execute()).Callback(() => { value = 2; });
Assert.NotSame(parentServiceMock.Object.Dependant, dependantServiceMock.Object);
parentServiceMock.Object.Dependant.Execute();
Assert.Equal(2, value);
}
我有以下设置:
DbContext:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public virtual DbSet<Album> Album { get; set; }
public ApplicationDbContext()
: base("DefaultConnection", throwIfV1Schema: false)
{
}
public static ApplicationDbContext Create()
{
return new ApplicationDbContext();
}
}
型号:
public class Album
{
public int AlbumID { get; set; }
[StringLength(150)]
public string Title { get; set; }
}
控制器:
public class AlbumController : Controller
{
ApplicationDbContext db = new ApplicationDbContext();
public AlbumController(ApplicationDbContext injectDb)
{
db = injectDb;
}
// POST: Albums/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
[Authorize(Roles = "Admin")]
public ActionResult DeleteConfirmed(int id)
{
Album album = db.Album.Find(id);
db.Album.Remove(album);
db.SaveChanges();
return RedirectToAction("Index");
}
}
我使用 Moq 和 xUnit 编写了单元测试来检查 DeleteConfirmed 功能:
public class AlbumsControllerTests
{
public static Mock<DbSet<T>> MockDbSet<T>(List<T> inputDbSetContent) where T : class
{
var DbSetContent = inputDbSetContent.AsQueryable();
var dbSet = new Mock<DbSet<T>>();
dbSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(DbSetContent.Provider);
dbSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(DbSetContent.Expression);
dbSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(DbSetContent.ElementType);
dbSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(() => inputDbSetContent.GetEnumerator());
dbSet.Setup(m => m.Add(It.IsAny<T>())).Callback<T>((s) => inputDbSetContent.Add(s));
dbSet.Setup(m => m.Remove(It.IsAny<T>())).Callback<T>((s) => inputDbSetContent.Remove(s));
return dbSet;
}
[Fact]
public void DeleteConfirmedTest()
{
// Arrange
var mockAlbumSet = MockDbSet(new List<Album> { });
Mock<ApplicationDbContext> sutDbContext = new Mock<ApplicationDbContext>() { CallBase = true };
sutDbContext.Setup(m => m.Album).Returns(mockAlbumSet.Object);
// Check if Album.Remove works inside this test
var albumToBeDeleted = new Album() { AlbumID = 1, Title = "TestAlbumName" };
sutDbContext.Object.Album.Add(albumToBeDeleted);
Assert.Equal(1, (from a in sutDbContext.Object.Album select a).Count());
sutDbContext.Object.Album.Remove(albumToBeDeleted);
Assert.Equal(0, (from a in sutDbContext.Object.Album select a).Count());
// Actual Test
sutDbContext.Object.Album.Add(albumToBeDeleted);
sutDbContext.Setup(m => m.Album.Find(It.IsAny<int>()))
.Returns(albumToBeDeleted);
AlbumController sut = new AlbumController(sutDbContext.Object);
var output = sut.DeleteConfirmed(1); // Throws NotImplementedException
// Assert
Assert.Equal(0, (from a in sutDbContext.Object.Album select a).Count());
}
}
测试在 db.Album.Remove(album) in DeleteConfirmed 中抛出以下异常:
System.NotImplementedException : The member 'Remove' has not been implemented on type 'DbSet
1Proxy' which inherits from 'DbSet
1'. Test doubles for 'DbSet`1' must provide implementations of methods and properties that are used.
正如您在 MockDbSet 方法主体中看到的那样,我为我的 Mock 设置了 Remove 方法,它在单元测试中工作得很好。你能解释一下为什么它在控制器内部不起作用吗?
如果您更改线路,您的测试将正常运行:
sutDbContext.Setup(m => m.Album.Find(It.IsAny<int>()))
.Returns(albumToBeDeleted);
收件人:
mockAlbumSet.Setup(x=>x.Find(It.IsAny<int>()))
.Returns(albumToBeDeleted);
当 sutDbContext.Album
被调用时,您将 sutDbContext 设置为 return mockAlbumSet.Object
,但是该行覆盖了您的设置,为 sutDbContext.Album
创建了一个新的模拟对象属性 并为该模拟创建了一个设置:
m.Album.Find(It.IsAny<int>()))
.Returns(albumToBeDeleted);
这是一个简单的测试,向您展示调用 class 的嵌套 属性 的设置,之前设置为 return a Mock.Object,将用新的 Mock.Object:
覆盖 属性public interface IParentService
{
IDependantService Dependant { get; }
}
public interface IDependantService
{
void Execute();
}
[Fact]
//This test passes
public void VerifyThatNestedMockSetupGeneratesNewMockObject()
{
var value = 0;
var parentServiceMock = new Mock<IParentService>();
var dependantServiceMock = new Mock<IDependantService>();
dependantServiceMock.Setup(x => x.Execute()).Callback(() => { value = 1; });
parentServiceMock.Setup(x => x.Dependant).Returns(dependantServiceMock.Object);
Assert.Same(parentServiceMock.Object.Dependant, dependantServiceMock.Object);
parentServiceMock.Setup(x => x.Dependant.Execute()).Callback(() => { value = 2; });
Assert.NotSame(parentServiceMock.Object.Dependant, dependantServiceMock.Object);
parentServiceMock.Object.Dependant.Execute();
Assert.Equal(2, value);
}