2015-10-16 79 views
3

我找到一份像樣的文章,讓我開始單元測試使用起訂量我的基於框架的實體應用程序時出現驗證懲戒EF6:https://msdn.microsoft.com/en-us/data/dn314429.aspx與上調用SaveChanges

我遇到這個問題是的的SaveChanges方法模擬似乎沒有像通常那樣觸發ValidateEntity方法。我在EntityTypeConfiguration中配置的驗證設置都不是DbEntityValidationException

例如,我的AddRoles_Fails_For_Empty_Name測試以確保服務不能添加名稱爲空的角色。 IsRequired()配置未被應用,或者ValidateEntity方法未被調用。我應該提到,如果我在Web應用程序中使用實際的上下文,它可以正常工作。

我在下面列出了一些相關的單元測試,DbContext和服務代碼。

我做錯了什麼嗎?是否有任何已知問題或解決方法?

角色DB地圖

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

回答

3

SaveChangesvirtual方法,這意味着你調用一個方法假....

您可以創建模擬CallBase = true,但它不是一個好主意(它錯過了主意UT的):

_context = new Mock<BlueMoonContext>(){ CallBase = true }; 

上面的代碼將使用實際實施BlueMoonContext任何方法/屬性這是不明確地設置。

RoleMap負責你的數據庫結構,你應該測試它作爲集成測試(使用數據庫)的一部分。

在我看來,你應該創建一個集成測試,以驗證完整性(例如,覆蓋RoleMap)與您的數據庫,並採用Throw建立覆蓋閉鎖段創建UT(這是你的單元的一部分):

_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規則(除非你使用的工具,如MsFakesTypeMock Isolator沒有辦法驗證ToTable/HasKey/Property被調用)。該課程正在EntityFramework(這是BCL的一部分)中使用,您不必驗證EntityFramework是否正常工作,您將驗證您的自定義規則是否已經集成並按預期工作(在this answer你可以閱讀不測試BCL類的原因)。因此在AccountService的UT中使用Moq。爲BlueMoonContextRoleMap(不含Moq)創建集成測試。

順便說@LadislavMrnka提供一個interesting way to test(integration test)EntityTypeConfiguration

+0

雖然我同意RoleMap確實是負責數據庫的結構,這也讓我像強制執行'Required'和'StringLength'簡單的驗證約束。爲了「正確」進行單元測試,是否需要剝離內置的驗證並將其放入其自己的層?或者我會不要使用Moq,只是創建另一個'IBlueMoonContext'的實現,它不會觸及數據庫進行測試? – Sam

+0

@Sam的答案太長以至於無法發表評論,所以我編輯了答案...... –

+0

所以,我最終做的是將驗證與SaveChanges()方法分開。我通過在構造函數中設置了'Configuration.ValidateOnSaveEnabled = false;'來實現這一點。然後我創建了一個輕量級的實體驗證器類,它封裝了DataAnnotations'Validator'類並將其注入到我的服務中。 – Sam