使用 SaveChanges 上发生的验证模拟 EF6
Mocking EF6 with validation that occurs on SaveChanges
我找到了一篇不错的文章,让我开始使用 Moq 对基于 Entity Framework 的应用程序进行单元测试:https://msdn.microsoft.com/en-us/data/dn314429.aspx
我遇到的这个问题是 Mock 的 SaveChanges
方法似乎没有像通常那样触发 ValidateEntity
方法。 None 我在 EntityTypeConfiguration
中配置的验证设置被抛出为 DbEntityValidationException
。
例如,我的 AddRoles_Fails_For_Empty_Name
测试以确保该服务无法添加具有空名称的角色。没有应用 IsRequired()
配置,或者没有调用 ValidateEntity
方法。我应该提到,如果我在网络应用程序中使用实际上下文,它可以正常工作。
我在下面包含了一些相关的单元测试、DbContext 和服务代码。
我做错了什么吗?是否有任何已知问题或解决方法?
角色数据库映射
public class RoleMap : EntityTypeConfiguration<Role>
{
public RoleMap()
{
ToTable("bm_Roles");
HasKey(r => r.Id);
Property(r => r.Name).IsRequired().HasMaxLength(100).HasIndex(new IndexAttribute("UX_Role_Name") { IsUnique = true });
Property(r => r.Description).HasMaxLength(500);
}
}
DbContext
public class BlueMoonContext : DbContext, IBlueMoonContext
{
public BlueMoonContext() : base("name=BlueMoon")
{
}
public DbSet<Role> Roles { get; set; }
public DbSet<User> Users { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Configurations.AddFromAssembly(typeof(BlueMoonContext).Assembly);
}
public void MarkAsModified<T>(T entity) where T : class
{
entity.ThrowIfNull("entity");
Entry<T>(entity).State = EntityState.Modified;
}
protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, IDictionary<object, object> items)
{
var result = base.ValidateEntity(entityEntry, items);
if (entityEntry.State == EntityState.Added || entityEntry.State == EntityState.Modified)
{
// Perform validations that require database lookups
if (entityEntry.Entity is Role)
{
ValidateRole((Role)entityEntry.Entity, result);
}
else if (entityEntry.Entity is User)
{
ValidateUser((User)entityEntry.Entity, result);
}
}
return result;
}
private void ValidateRole(Role role, DbEntityValidationResult result)
{
if (role.Name.HasValue() && !Roles.NameAvailable(role.Name, role.Id))
{
result.ValidationErrors.Add(new DbValidationError("Name", "Already in use"));
}
}
private void ValidateUser(User user, DbEntityValidationResult result)
{
if (user.UserName.HasValue() && !Users.UserNameAvailable(user.UserName, user.Id))
{
result.ValidationErrors.Add(new DbValidationError("UserName", "Already in use"));
}
if (user.Email.HasValue() && !Users.UserNameAvailable(user.UserName, user.Id))
{
result.ValidationErrors.Add(new DbValidationError("Email", "Already in use"));
}
}
}
账户服务
public class AccountService : BaseService, IAccountService
{
private IPasswordHasher _passwordHasher;
public AccountService(IBlueMoonContext context, IPasswordHasher passwordHasher) : base(context)
{
_passwordHasher = passwordHasher;
}
public ServiceResult CreateRole(Role role)
{
role.ThrowIfNull("role");
Context.Roles.Add(role);
return Save();
}
// Copied from base service class
protected ServiceResult Save()
{
var result = new ServiceResult();
try
{
Context.SaveChanges();
}
catch (DbEntityValidationException validationException)
{
foreach (var validationError in validationException.EntityValidationErrors)
{
foreach (var error in validationError.ValidationErrors)
{
result.AddError(error.ErrorMessage, error.PropertyName);
}
}
}
return result;
}
}
单元测试
[TestFixture]
public class AccountServiceTests : BaseTest
{
protected Mock<MockBlueMoonContext> _context;
private IAccountService _accountService;
[TestFixtureSetUp]
public void Setup()
{
_context = new Mock<BlueMoonContext>();
var data = new List<Role>
{
new Role { Id = 1, Name = "Super Admin" },
new Role { Id = 2, Name = "Catalog Admin" },
new Role { Id = 3, Name = "Order Admin" }
}.AsQueryable();
var roleSet = CreateMockSet<Role>(data);
roleSet.Setup(m => m.Find(It.IsAny<object[]>())).Returns<object[]>(ids => data.FirstOrDefault(d => d.Id == (int)ids[0]));
_context.Setup(m => m.Roles).Returns(roleSet.Object);
// _context.Setup(m => m.SaveChanges()).Returns(0);
_accountService = new AccountService(_context.Object, new CryptoPasswordHasher());
}
[Test]
public void AddRole_Fails_For_Empty_Name()
{
var role = new Role { Id = 4, Name = "" };
var result = _accountService.CreateRole(role);
Assert.False(result.Success);
}
}
SaveChanges
是一个 virtual
方法,这意味着您调用了一个假方法....
你可以创建你的 mock CallBase = true
,但这不是一个好主意(它错过了 UT 的想法):
_context = new Mock<BlueMoonContext>(){ CallBase = true };
以上代码将对任何未明确设置的 method/property 使用 BlueMoonContext
的实际实现。
RoleMap
负责您的数据库结构,您应该将其作为集成测试(与数据库)的一部分进行测试。
在我看来,您应该创建一个集成测试来验证数据库的完整性(例如;覆盖 RoleMap
),并使用 Throw
设置创建一个 UT 以覆盖 catch 部分(它是您 单元 的一部分):
_contest.Setup(x => x.SaveChanges())
.Throws(new DbEntityValidationException());
编辑以回答评论中的 OP 问题
不,您不必分离内置验证,您必须创建另一个测试(集成测试)。在此测试中,您将验证验证行为:插入一个非法实体,预计会引发异常(使用 ExpectedExceptionAttribute
),然后验证数据库是否为空...要应用此行为,请使用此模式:
try
{
\...
\try to commit
}
catch(DbEntityValidationException ex)
{
\do some validation, then:
throw;\for ExpectedExceptionAttribute
}
我查看了 EntityTypeConfiguration
的 api,我没有看到任何允许 UT 规则的联系人(除非你使用像 MsFakes
、[=23= 这样的工具] 无法验证调用了 ToTable/HasKey/Property
)。在集成测试中,class 在 EntityFramework
(它是 BCL 的一部分)中使用,您不必验证 EntityFramework
是否正常工作,您将验证您的自定义规则已集成并按预期工作(在 中,您可以阅读不测试 BCL classes 的原因)。
所以在AccountService
的UT中使用Moq
。为 BlueMoonContext
和 RoleMap
(没有 Moq
)创建集成测试。
顺便说一句,@LadislavMrnka 提供了一个 interesting way to test(integration test) EntityTypeConfiguration
我找到了一篇不错的文章,让我开始使用 Moq 对基于 Entity Framework 的应用程序进行单元测试:https://msdn.microsoft.com/en-us/data/dn314429.aspx
我遇到的这个问题是 Mock 的 SaveChanges
方法似乎没有像通常那样触发 ValidateEntity
方法。 None 我在 EntityTypeConfiguration
中配置的验证设置被抛出为 DbEntityValidationException
。
例如,我的 AddRoles_Fails_For_Empty_Name
测试以确保该服务无法添加具有空名称的角色。没有应用 IsRequired()
配置,或者没有调用 ValidateEntity
方法。我应该提到,如果我在网络应用程序中使用实际上下文,它可以正常工作。
我在下面包含了一些相关的单元测试、DbContext 和服务代码。
我做错了什么吗?是否有任何已知问题或解决方法?
角色数据库映射
public class RoleMap : EntityTypeConfiguration<Role>
{
public RoleMap()
{
ToTable("bm_Roles");
HasKey(r => r.Id);
Property(r => r.Name).IsRequired().HasMaxLength(100).HasIndex(new IndexAttribute("UX_Role_Name") { IsUnique = true });
Property(r => r.Description).HasMaxLength(500);
}
}
DbContext
public class BlueMoonContext : DbContext, IBlueMoonContext
{
public BlueMoonContext() : base("name=BlueMoon")
{
}
public DbSet<Role> Roles { get; set; }
public DbSet<User> Users { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Configurations.AddFromAssembly(typeof(BlueMoonContext).Assembly);
}
public void MarkAsModified<T>(T entity) where T : class
{
entity.ThrowIfNull("entity");
Entry<T>(entity).State = EntityState.Modified;
}
protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, IDictionary<object, object> items)
{
var result = base.ValidateEntity(entityEntry, items);
if (entityEntry.State == EntityState.Added || entityEntry.State == EntityState.Modified)
{
// Perform validations that require database lookups
if (entityEntry.Entity is Role)
{
ValidateRole((Role)entityEntry.Entity, result);
}
else if (entityEntry.Entity is User)
{
ValidateUser((User)entityEntry.Entity, result);
}
}
return result;
}
private void ValidateRole(Role role, DbEntityValidationResult result)
{
if (role.Name.HasValue() && !Roles.NameAvailable(role.Name, role.Id))
{
result.ValidationErrors.Add(new DbValidationError("Name", "Already in use"));
}
}
private void ValidateUser(User user, DbEntityValidationResult result)
{
if (user.UserName.HasValue() && !Users.UserNameAvailable(user.UserName, user.Id))
{
result.ValidationErrors.Add(new DbValidationError("UserName", "Already in use"));
}
if (user.Email.HasValue() && !Users.UserNameAvailable(user.UserName, user.Id))
{
result.ValidationErrors.Add(new DbValidationError("Email", "Already in use"));
}
}
}
账户服务
public class AccountService : BaseService, IAccountService
{
private IPasswordHasher _passwordHasher;
public AccountService(IBlueMoonContext context, IPasswordHasher passwordHasher) : base(context)
{
_passwordHasher = passwordHasher;
}
public ServiceResult CreateRole(Role role)
{
role.ThrowIfNull("role");
Context.Roles.Add(role);
return Save();
}
// Copied from base service class
protected ServiceResult Save()
{
var result = new ServiceResult();
try
{
Context.SaveChanges();
}
catch (DbEntityValidationException validationException)
{
foreach (var validationError in validationException.EntityValidationErrors)
{
foreach (var error in validationError.ValidationErrors)
{
result.AddError(error.ErrorMessage, error.PropertyName);
}
}
}
return result;
}
}
单元测试
[TestFixture]
public class AccountServiceTests : BaseTest
{
protected Mock<MockBlueMoonContext> _context;
private IAccountService _accountService;
[TestFixtureSetUp]
public void Setup()
{
_context = new Mock<BlueMoonContext>();
var data = new List<Role>
{
new Role { Id = 1, Name = "Super Admin" },
new Role { Id = 2, Name = "Catalog Admin" },
new Role { Id = 3, Name = "Order Admin" }
}.AsQueryable();
var roleSet = CreateMockSet<Role>(data);
roleSet.Setup(m => m.Find(It.IsAny<object[]>())).Returns<object[]>(ids => data.FirstOrDefault(d => d.Id == (int)ids[0]));
_context.Setup(m => m.Roles).Returns(roleSet.Object);
// _context.Setup(m => m.SaveChanges()).Returns(0);
_accountService = new AccountService(_context.Object, new CryptoPasswordHasher());
}
[Test]
public void AddRole_Fails_For_Empty_Name()
{
var role = new Role { Id = 4, Name = "" };
var result = _accountService.CreateRole(role);
Assert.False(result.Success);
}
}
SaveChanges
是一个 virtual
方法,这意味着您调用了一个假方法....
你可以创建你的 mock CallBase = true
,但这不是一个好主意(它错过了 UT 的想法):
_context = new Mock<BlueMoonContext>(){ CallBase = true };
以上代码将对任何未明确设置的 method/property 使用 BlueMoonContext
的实际实现。
RoleMap
负责您的数据库结构,您应该将其作为集成测试(与数据库)的一部分进行测试。
在我看来,您应该创建一个集成测试来验证数据库的完整性(例如;覆盖 RoleMap
),并使用 Throw
设置创建一个 UT 以覆盖 catch 部分(它是您 单元 的一部分):
_contest.Setup(x => x.SaveChanges())
.Throws(new DbEntityValidationException());
编辑以回答评论中的 OP 问题
不,您不必分离内置验证,您必须创建另一个测试(集成测试)。在此测试中,您将验证验证行为:插入一个非法实体,预计会引发异常(使用 ExpectedExceptionAttribute
),然后验证数据库是否为空...要应用此行为,请使用此模式:
try
{
\...
\try to commit
}
catch(DbEntityValidationException ex)
{
\do some validation, then:
throw;\for ExpectedExceptionAttribute
}
我查看了 EntityTypeConfiguration
的 api,我没有看到任何允许 UT 规则的联系人(除非你使用像 MsFakes
、[=23= 这样的工具] 无法验证调用了 ToTable/HasKey/Property
)。在集成测试中,class 在 EntityFramework
(它是 BCL 的一部分)中使用,您不必验证 EntityFramework
是否正常工作,您将验证您的自定义规则已集成并按预期工作(在
所以在AccountService
的UT中使用Moq
。为 BlueMoonContext
和 RoleMap
(没有 Moq
)创建集成测试。
顺便说一句,@LadislavMrnka 提供了一个 interesting way to test(integration test) EntityTypeConfiguration