我正在使用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);
}
}
正如您所看到的,在[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();
}
正如您所看到的,如果我正在尝试保存已存在的Contact
,我正在做的就是将State
标志设置为Modified
并让EF处理所有其余的。
在运行时运行良好 - 但在测试中,它会导致测试代码想要连接到数据库 - 我手头没有。
那么我还需要做些什么才能使用我的EF Mocking基础设施对这行代码进行单元测试?它可以完成吗?
ModelContext.Entry(contactToSave).State = EntityState.Modified;
答案 0 :(得分:1)
DbContext.Entry
不是虚拟,因此moq无法覆盖它。
您基本上是在尝试对EF进行单元测试,而微软在发布之前就会对其进行测试。最好使用实际的后备数据库与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
}
并让您的上下文实现从中派生
public partial class MyModel : DbContext, IMyModelContext {
//...
}
类应该依赖于抽象而不是结核。
public class ContactsUseCase {
private readonly IMyModelContext ModelContext;
public ContactsUseCase(IMyModelContext context) {
ModelContext = context;
}
//...
}
您仍然可以使用模拟包来模拟您的数据库集,但现在您也可以灵活地正确模拟上下文。
[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());
}
}