2010-10-07 118 views
1

比方說,我有這樣的單元測試:TDD和MVC模型綁定

[Test] 
    public void LastNameShouldNotBeEmpty() 
    { 
     ExampleController controller = new ExampleController(); 

     Person editedPerson = new Person { FirstName = "j", LastName = "" }; 
     controller.EditPerson(editedPerson); 

     Assert.AreEqual(controller.ModelState.IsValid, false); 
    } 

而這種代碼:

public class ExampleController : Controller 
{ 
    public ActionResult EditPerson(int personId) 
    { 
     // Serve up a view, whatever 
     return View(Person.LoadPerson(personId)); 
    } 

    [HttpPost] 
    public ActionResult EditPerson(Person person) 
    { 
     if (ModelState.IsValid) 
     { 
      // TODO - actually save the modified person, whatever 
     } 

     return View(person); 
    } 
} 

public class Person 
{ 
    public string FirstName { get; set; } 
    [Required] public string LastName { get; set; } 
} 

它困擾着我,如果我TDD了一個要求,即姓氏不能是空的,我不能滿足使用DataAnnotation屬性的測試(在Person上的LastName聲明之前的[Required]),因爲當從單元測試調用控制器的操作方法時,MVC基礎結構沒有機會應用在模型綁定過程中進行驗證。

(如果我手動在控制器的EditPerson方法進行驗證,不過,並添加一個錯誤的ModelState中,這將是從一個單元測試驗證。)

我缺少的東西?我想指定使用單元測試我的系統的驗證行爲,但我不知道如何滿足一個單元測試,除非我完全放棄DataAnnotation屬性和手動執行內我的控制器的操作方法驗證。

我希望我的問題的意圖是明確的;有沒有辦法強制真正的模型綁定從自動單元測試執行(包括驗證行爲,以測試我沒有忘記重要的驗證屬性)?

傑夫

回答

0

我個人認爲你應該有單元測試來測試MVC範圍之外的屬性本身。這應該是你的模型測試的一部分,而不是你的控制器測試。你沒有寫MVC驗證碼,所以不要試着去測試它!只要測試你的對象具有你期望的正確屬性的事實。

這是很粗糙的,但你的想法...

[Test] 
public void LastNameShouldBeRequired() 
{ 
    var personType = typeof(Person); 
    var lastNamePropInfo = objType.GetProperty("LastName"); 
    var requiredAttrs = lastNamePropInfo.GetCustomAttributes(typeof(RequiredAttribute), true).OfType<RequiredAttribute>(); 
    Assert.IsTrue(requiredAttrs.Any()); 
} 

然後在你的MVC測試你只是測試控制器的流量,而不是數據註解的有效性。你可以告訴ModelState中,它是無效的測試如果驗證通過手動添加一個錯誤,因爲你注意到失敗等發生了什麼流量。然後,這是對你的控制器負責的一個非常可控的測試,而不是框架爲你做什麼。

+0

我明白你在說什麼,我當然不需要測試MVC基礎結構代碼的正確行爲。我要測試的是,我記得要添加正確的屬性,而不是基礎設施是否正確驗證它們(因爲我相信它確實)。你的解決方案也可以。我只是不想爲基於批註的驗證編寫完全不同的單元測試,而不是使用動作方法中的自定義邏輯實現的驗證。我想出了一個替代方案,可以讓我以相同的方式指定兩個測試。 – blaster 2010-10-08 17:44:24

+0

我認爲最好使用更自然的方式,例如測試使用驗證器隔離實體(它是自己的單元測試)。恕我直言,通過檢查註釋來做這件事不是一種自然的單元測試方式。 – Braulio 2012-03-08 17:08:12

1

我同意這不是一個非常令人滿意的情況。不過,也有一些簡單的解決辦法:解決此問題

  1. 工作通過反映在數據實體和尋找必要的驗證特性(這是我目前在做什麼)。它比聽起來容易得多。

  2. 建立自己的驗證,反映了視圖模型參數類型和驗證它。用它來驗證你的單元測試是否設置了適當的驗證屬性。假設您的驗證類是無缺陷的,它應該等同於ASP.NET MVC ModelBinder中的驗證算法。我已經爲了一個不同的目的而編寫了這樣一個驗證器類,它不比第一個選項困難得多。

0

我不喜歡那個檢查個人屬性的存在的測試中,它使測試作用不像文檔和緊密結合我的ASP.NET MVC的理解(這可能是錯誤的),而不是緊密地結合在一起業務需求(我關心的)。

因此對於這類事情,我最終編寫了集成測試,直接或通過瀏覽器使用WatiN生成HTTP請求。一旦你完成了這個任務,你可以在沒有額外的MVC抽象的情況下編寫測試,測試記錄你真正關心的是什麼。也就是說,這樣的測試很慢。

我也做過一些事情,我的集成測試可以發出後門請求,這會導致在服務器進程中加載​​測試夾具。這個文本夾具將臨時覆蓋我的IOC容器中的綁定......這樣可以減少集成測試的設置,儘管在這一點上它們只是半集成測試。

例如,我可能會用一個模擬控制器替換一個控制器,該控制器將驗證用期望的參數調用動作方法。更通常的情況下,我將網站的數據源替換爲我預先填充的另一個數據源。

7

下面是我提出的一個解決方案。它要求將一行代碼添加到單元測試中,但是我發現它讓我不在乎是否通過操作方法中的通過自定義代碼的屬性強制實施驗證,這感覺像測試更精神指定結果而不是實施。即使驗證來自數據註釋,它也允許測試以書面形式傳遞。請注意,新行權EditPerson動作方法的調用上面:

[Test] 
    public void LastNameShouldNotBeEmpty() 
    { 
     FakeExampleController controller = new FakeExampleController(); 

     Person editedPerson = new Person { FirstName = "j", LastName = "" }; 

     // Performs the same attribute-based validation that model binding would perform 
     controller.ValidateModel(editedPerson); 

     controller.EditPerson(editedPerson); 

     Assert.AreEqual(false, controller.ModelState.IsValid); 
     Assert.AreEqual(true, controller.ModelState.Keys.Contains("LastName")); 
     Assert.AreEqual("Last name cannot be blank", controller.ModelState["LastName"].Errors[0].ErrorMessage); 
    } 

ValidateModel實際上是我創建一個擴展方法(控制器確實有ValidateModel方法,但它是受保護的,因此它不能被調用從單元測試直接)。它使用反射調用控制器上的受保護的TryValidateModel()方法,該方法將觸發基於註釋的驗證,就像動作方法確實通過MVC.NET基礎結構調用一樣。

public static class Extensions 
{ 
    public static void ValidateModel<T>(this Controller controller, T modelObject) 
    { 
     if (controller.ControllerContext == null) 
      controller.ControllerContext = new ControllerContext(); 

     Type type = controller.GetType(); 
     MethodInfo tryValidateModelMethod = 
      type.GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance).Where(
       mi => mi.Name == "TryValidateModel" && mi.GetParameters().Count() == 1).First(); 

     tryValidateModelMethod.Invoke(controller, new object[] {modelObject}); 
    } 
} 

它似乎能夠以最小的侵入性工作,儘管可能存在我不知道的後果。 。 。

傑夫

+1

+1 - 調用TryValidateModel爲我工作,雖然我去了派生控制器類而不是反射。順便說一句,你不需要BindingFlags.Public,該方法不一定是通用的。 – 2010-12-31 17:28:23

0

我們可以利用Validator輔助類做TDD與模型驗證。你可以找到一個詳細的博客關於測試駕駛模型驗證here