在控制器内部调用时,模拟的 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 'DbSet1'. 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);
}