单元测试EF的状态管理代码

mar*_*c_s 5 c# unit-testing moq mocking entity-framework-6

我正在使用Entity Framework(针对SQL Server数据库)为我的ASP.NET MVC应用程序编写一堆单元测试.

我正在使用Rowan Miller优秀的Nuget软件包"EntityFramework.Testing"和"EntityFramework.Testing.Moq"来允许我对EF代码进行单元测试(实际上没有真正的SQL Server数据库).

这是我的NUnit 3.5测试夹具(实际上,它有更多的测试 - 但它只是为了展示如何设置):

[TestFixture]
public class ContactsUseCaseTests : MyUnitTestBase
{
    private Mock<MyModel> _mockDbContext;
    private MockDbSet<Contact> _mockDbSetContact;
    private IContactsUseCase _usecase;

    [SetUp]
    public void InitializeTest()
    {
        SetupTestData();
        _usecase = new ContactsUseCase(_mockDbContext.Object);
    }

    [Test]
    public void TestSaveEntryNotNewButNotFound()
    {
        // Arrange
        Contact contact = new Contact { ContactId = 99, FirstName = "Leo", LastName = "Miller" };

        // Act
        _usecase.SaveContact(contact, false);

        // Assert
        _mockDbSetContact.Verify(x => x.Add(It.IsAny<Contact>()), Times.Once);
        _mockDbContext.Verify(x => x.SaveChanges(), Times.Once);
    }

    private void SetupTestData()
    {
        var contacts = new List<Contact>();

        contacts.Add(new Contact { ContactId = 12, FirstName = "Joe", LastName = "Smith" });
        contacts.Add(new Contact { ContactId = 17, FirstName = "Daniel", LastName = "Brown" });
        contacts.Add(new Contact { ContactId = 19, FirstName = "Frank", LastName = "Singer" });

        _mockDbSetContact = new MockDbSet<Contact>()
            .SetupAddAndRemove()
            .SetupSeedData(contacts)
            .SetupLinq();

        _mockDbContext = new Mock<MyModel>();
        _mockDbContext.Setup(c => c.ContactList).Returns(_mockDbSetContactList.Object);
        _mockDbContext.Setup(c => c.Contact).Returns(_mockDbSetContact.Object);
    }
}
Run Code Online (Sandbox Code Playgroud)

正如你所看到的,在[SetUp]方法中,我正在调用SetupTestData哪个正在创建Mock<MyModel>用于模拟整个DbContext,并设置一个MockDbSet<Contact>来处理我的联系人.

大多数测试对这种设置都很好 - 直到我在SaveContact这里遇到这个方法:

public void SaveContact(Contact contactToSave, bool isNew) {
    if (isNew) {
        ModelContext.Contact.Add(contactToSave);
    } else {
        ModelContext.Entry(contactToSave).State = EntityState.Modified;
    }
    ModelContext.SaveChanges();
}
Run Code Online (Sandbox Code Playgroud)

正如你所看到的,如果我正在尝试保存Contact已存在的,我正在做的就是设置它的State标志Modified并让EF处理所有其余的.

伟大工程在运行时 -但在这里的测试中,它会导致测试代码要连接到数据库-我没有在眼前.

那么我还需要做些什么才能使用我的EF Mocking基础设施对这行代码进行单元测试呢?它可以完成吗?

ModelContext.Entry(contactToSave).State = EntityState.Modified;
Run Code Online (Sandbox Code Playgroud)

Nko*_*osi 1

DbContext.Entry不是虚拟的,因此起订量无法覆盖它。

您基本上是在尝试对 EF 进行单元测试,这是 Microsoft 在发布之前就已经完成的。最好使用实际的支持数据库来执行 EF 集成测试。

尽管如此,您可以考虑抽象对模型的访问。

public interface IMyModelContext : IDisposable {
    DbSet<Contact> Contact { get; }
    int SaveChanges();
    DbEntityEntry Entry(object entity);
    DbEntityEntry<TEntity> Entry<TEntity>(TEntity entity) where TEntity : class;
    //..other needed members
}
Run Code Online (Sandbox Code Playgroud)

并让你的上下文实现从中派生

public partial class MyModel : DbContext, IMyModelContext {
    //...
}
Run Code Online (Sandbox Code Playgroud)

类应该依赖于抽象而不是具体。

public class ContactsUseCase {
    private readonly IMyModelContext ModelContext;

    public ContactsUseCase(IMyModelContext context) {
        ModelContext = context;
    }

    //...
}
Run Code Online (Sandbox Code Playgroud)

您仍然可以使用模拟包来模拟您的数据库集,但现在您也可以灵活地正确模拟上下文。

[TestFixture]
public class ContactsUseCaseTests : MyUnitTestBase {
    private Mock<IMyModelContext> _mockDbContext;
    private MockDbSet<Contact> _mockDbSetContact;
    private IContactsUseCase _usecase;

    [SetUp]
    public void InitializeTest() {
        SetupTestData();
        _usecase = new ContactsUseCase(_mockDbContext.Object);
    }

    [Test]
    public void TestSaveEntryNotNewButNotFound() {
        // Arrange
        Contact contact = new Contact { ContactId = 99, FirstName = "Leo", LastName = "Miller" };

        // Act
        _usecase.SaveContact(contact, false);

        // Assert
        _mockDbSetContact.Verify(x => x.Add(It.IsAny<Contact>()), Times.Never);
        _mockDbContext.Verify(x => x.SaveChanges(), Times.Once);
    }

    private void SetupTestData() {
        var contacts = new List<Contact>();

        contacts.Add(new Contact { ContactId = 12, FirstName = "Joe", LastName = "Smith" });
        contacts.Add(new Contact { ContactId = 17, FirstName = "Daniel", LastName = "Brown" });
        contacts.Add(new Contact { ContactId = 19, FirstName = "Frank", LastName = "Singer" });

        _mockDbSetContact = new MockDbSet<Contact>()
            .SetupAddAndRemove()
            .SetupSeedData(contacts)
            .SetupLinq();

        _mockDbContext = new Mock<IMyModelContext>();
        _mockDbContext.Setup(c => c.ContactList).Returns(_mockDbSetContactList.Object);
        _mockDbContext.Setup(c => c.Contact).Returns(_mockDbSetContact.Object);

        _mockDbContext.Setup(c => c.Entry(It.IsAny<Contact>()).Returns(new DbEntityEntry());
    }
}
Run Code Online (Sandbox Code Playgroud)