Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unit-testing EF's state management code

I'm writing a bunch of unit tests for my ASP.NET MVC app using Entity Framework (against a SQL Server database).

I'm using Rowan Miller's excellent Nuget packages "EntityFramework.Testing" and "EntityFramework.Testing.Moq" to allow me to unit test EF code (without actually having a real SQL Server database around).

This is my NUnit 3.5 test fixture (in reality, it has a lot more tests - but it's just to show how things are set up):

[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);
    }
}

As you can see, in the [SetUp] method, I'm calling SetupTestData which is creating the Mock<MyModel> for mocking the entire DbContext, and it sets up a MockDbSet<Contact> to handle my contacts.

Most tests works just fine against this setup - until I came across the SaveContact method here:

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

As you can see, if I'm trying to save a Contact that already exists, all I'm doing is setting it's State flag to Modified and letting EF handle all the rest.

Works great at runtime - but here in the test, it causes the test code to want to connect to the database - which I don't have at hand.

So what do I need to do additionally to make it possible to unit-test this line of code using my EF Mocking infrastructure? Can it be done at all?

ModelContext.Entry(contactToSave).State = EntityState.Modified;
like image 432
marc_s Avatar asked Nov 07 '22 14:11

marc_s


1 Answers

DbContext.Entry is not virtual so moq is unable to override it.

You are basically trying to unit test EF, which Microsoft would have done before release. It is better to perform integration tests with EF using an actual backing database.

That said however, you could consider abstracting the access to your model.

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
}

and having your context implementation derive from it

public partial class MyModel : DbContext, IMyModelContext {
    //...
}

Classes should depend on abstractions and not on concretions.

public class ContactsUseCase {
    private readonly IMyModelContext ModelContext;

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

    //...
}

You can still use the mocking package to mock your db sets, but now you have the flexibility to properly mock the context as well.

[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());
    }
}
like image 133
Nkosi Avatar answered Nov 14 '22 23:11

Nkosi