MVC 2プレビュー1でDataAnnotation検証を使用しているときに、エンティティを検証するときにコントローラーアクションがModelStateに正しいエラーを入れていることをテストするにはどうすればよいですか?
説明するコード。まず、アクション:
[HttpPost]
public ActionResult Index(BlogPost b)
{
if(ModelState.IsValid)
{
_blogService.Insert(b);
return(View("Success", b));
}
return View(b);
}
そして、ここに失敗する単体テストがありますが、合格するはずですが、そうではありません(MbUnitとMoqを使用):
[Test]
public void When_processing_invalid_post_HomeControllerModelState_should_have_at_least_one_error()
{
// arrange
var mockRepository = new Mock<IBlogPostSVC>();
var homeController = new HomeController(mockRepository.Object);
// act
var p = new BlogPost { Title = "test" }; // date and content should be required
homeController.Index(p);
// assert
Assert.IsTrue(!homeController.ModelState.IsValid);
}
この質問に加えて、should検証をテストしていますが、この方法でテストする必要がありますか?
古い投稿をネクロするのは嫌いですが、私は自分の考えを追加したいと思いました(この問題があり、答えを探している間にこの投稿に出くわしたからです)。
ここで本当にテストしたいのは、検証が失敗したときにコントローラーが期待することを実行することです。それがあなたのコードであり、あなたの期待です。テストが必要なのはこれだけだとわかったら、テストは簡単です。
[test]
public void TestInvalidPostBehavior()
{
// arrange
var mockRepository = new Mock<IBlogPostSVC>();
var homeController = new HomeController(mockRepository.Object);
var p = new BlogPost();
homeController.ViewData.ModelState.AddModelError("Key", "ErrorMessage"); // Values of these two strings don't matter.
// What I'm doing is setting up the situation: my controller is receiving an invalid model.
// act
var result = (ViewResult) homeController.Index(p);
// assert
result.ForView("Index")
Assert.That(result.ViewData.Model, Is.EqualTo(p));
}
私は同じ問題を抱えていましたが、ポールの回答とコメントを読んだ後、ビューモデルを手動で検証する方法を探しました。
このチュートリアル を見つけました。これは、DataAnnotationsを使用するViewModelを手動で検証する方法を説明しています。キーコードスニペットは、投稿の終わり頃です。
コードを少し修正しました-チュートリアルでは、TryValidateObjectの4番目のパラメーターが省略されています(validateAllProperties)。検証するすべての注釈を取得するには、これをtrueに設定する必要があります。
さらに、ViewModel検証のテストを簡単にするために、コードを汎用メソッドにリファクタリングしました。
public static void ValidateViewModel<TViewModel, TController>(this TController controller, TViewModel viewModelToValidate)
where TController : ApiController
{
var validationContext = new ValidationContext(viewModelToValidate, null, null);
var validationResults = new List<ValidationResult>();
Validator.TryValidateObject(viewModelToValidate, validationContext, validationResults, true);
foreach (var validationResult in validationResults)
{
controller.ModelState.AddModelError(validationResult.MemberNames.FirstOrDefault() ?? string.Empty, validationResult.ErrorMessage);
}
}
これまでのところ、これは非常にうまく機能しています。
テストでhomeController.Indexメソッドを呼び出すときは、検証を実行するMVCフレームワークを使用していないため、ModelState.IsValidは常にtrueになります。このコードでは、アンビエント検証を使用するのではなく、コントローラーでヘルパーValidateメソッドを直接呼び出します。 DataAnnotations(私たちはNHibernate.Validatorsを使用しています)の経験はあまりありませんが、コントローラー内からValidateを呼び出す方法について他の誰かがガイダンスを提供できるかもしれません。
今日私はこれを調査していたのですが、ロバート・ヘルナンデス(MVP)による このブログ投稿 が見つかりました。これにより、エンティティの検証時にModelStateに正しいエラーが記録されます。
Model.IsValid値を更新できるように、テストケースでModelBindersを使用しています。
var form = new FormCollection();
form.Add("Name", "0123456789012345678901234567890123456789");
var model = MvcModelBinder.BindModel<AddItemModel>(controller, form);
ViewResult result = (ViewResult)controller.Add(model);
MvcModelBinder.BindModelメソッドを次のように使用します(基本的に、MVCフレームワークで内部的に使用されるコードと同じです)。
public static TModel BindModel<TModel>(Controller controller, IValueProvider valueProvider) where TModel : class
{
IModelBinder binder = ModelBinders.Binders.GetBinder(typeof(TModel));
ModelBindingContext bindingContext = new ModelBindingContext()
{
FallbackToEmptyPrefix = true,
ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(TModel)),
ModelName = "NotUsedButNotNull",
ModelState = controller.ModelState,
PropertyFilter = (name => { return true; }),
ValueProvider = valueProvider
};
return (TModel)binder.BindModel(controller.ControllerContext, bindingContext);
}
検証については気にするが、実装方法については気にしない場合、DataAnnotations、ModelBinders、またはActionFilterAttributesを使用して実装されているかどうかに関係なく、抽象化の最高レベルでのアクションメソッドの検証のみに関心がある場合、次のようにXania.AspNet.Simulator nugetパッケージを使用できます。
install-package Xania.AspNet.Simulator
-
var action = new BlogController()
.Action(c => c.Index(new BlogPost()), "POST");
var modelState = action.ValidateRequest();
modelState.IsValid.Should().BeFalse();
DataAnnotationsを放棄するため、これはあなたの質問に正確に答えませんが、他の人がコントローラーのテストを書くのを助けるかもしれないので、私はそれを追加します:
System.ComponentModel.DataAnnotationsによって提供される検証を使用せずに、AddModelError
メソッドとその他の検証メカニズムを使用して、ViewData.ModelStateオブジェクトを引き続き使用するオプションがあります。例えば:
_public ActionResult Create(CompetitionEntry competitionEntry)
{
if (competitionEntry.Email == null)
ViewData.ModelState.AddModelError("CompetitionEntry.Email", "Please enter your e-mail");
if (ModelState.IsValid)
{
// insert code to save data here...
// ...
return Redirect("/");
}
else
{
// return with errors
var viewModel = new CompetitionEntryViewModel();
// insert code to populate viewmodel here ...
// ...
return View(viewModel);
}
}
_
これにより、DataAnnotations
を使用せずに、MVCが生成するHtml.ValidationMessageFor()
を利用できます。 AddModelError
で使用するキーが、ビューが検証メッセージに期待するものと一致することを確認する必要があります。
検証はMVCフレームワークによって自動的に行われるのではなく、明示的に行われるため、コントローラーはテスト可能になります。
ARMが最良の答えを持っていることに同意します。組み込みの検証ではなく、コントローラーの動作をテストします。
ただし、Model/ViewModelに正しい検証属性が定義されていることを単体テストすることもできます。 ViewModelが次のようになっているとしましょう:
public class PersonViewModel
{
[Required]
public string FirstName { get; set; }
}
この単体テストは、[Required]
属性の存在をテストします。
[TestMethod]
public void FirstName_should_be_required()
{
var propertyInfo = typeof(PersonViewModel).GetProperty("FirstName");
var attribute = propertyInfo.GetCustomAttributes(typeof(RequiredAttribute), false)
.FirstOrDefault();
Assert.IsNotNull(attribute);
}
ARMとは対照的に、私は墓掘りに問題はありません。だからここに私の提案があります。 Giles Smithの答えに基づいて構築され、ASP.NET MVC4で動作します(質問はMVC 2についてのものであることがわかりますが、答えを探す際にGoogleは差別せず、MVC2でテストできません)。一般的な静的メソッドであるため、テストコントローラーに配置します。コントローラーには検証に必要なものがすべて揃っています。そのため、テストコントローラーは次のようになります。
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Wbe.Mvc;
protected class TestController : Controller
{
public void TestValidateModel(object Model)
{
ValidationContext validationContext = new ValidationContext(Model, null, null);
List<ValidationResult> validationResults = new List<ValidationResult>();
Validator.TryValidateObject(Model, validationContext, validationResults, true);
foreach (ValidationResult validationResult in validationResults)
{
this.ModelState.AddModelError(String.Join(", ", validationResult.MemberNames), validationResult.ErrorMessage);
}
}
}
もちろん、クラスは保護されたインナークラスである必要はありません。それが現在の使用方法ですが、おそらくそのクラスを再利用するつもりです。 Niceデータ注釈属性で装飾されたモデルMyModelがある場合、テストは次のようになります。
[TestMethod()]
public void ValidationTest()
{
MyModel item = new MyModel();
item.Description = "This is a unit test";
item.LocationId = 1;
TestController testController = new TestController();
testController.TestValidateModel(item);
Assert.IsTrue(testController.ModelState.IsValid, "A valid model is recognized.");
}
このセットアップの利点は、すべてのモデルのテストにテストコントローラーを再利用でき、コントローラーについてもう少しモックを作成したり、コントローラーが持つ保護されたメソッドを使用したりできることです。
それが役に立てば幸い。
@ giles-smithの答えは私の好みのアプローチですが、実装は単純化できます:
public static void ValidateViewModel(this Controller controller, object viewModelToValidate)
{
var validationContext = new ValidationContext(viewModelToValidate, null, null);
var validationResults = new List<ValidationResult>();
Validator.TryValidateObject(viewModelToValidate, validationContext, validationResults, true);
foreach (var validationResult in validationResults)
{
controller.ModelState.AddModelError(validationResult.MemberNames.FirstOrDefault() ?? string.Empty, validationResult.ErrorMessage);
}
}
@ giles-smithの回答とコメントに基づく、Web APIの場合:
public static void ValidateViewModel<TViewModel, TController>(this TController controller, TViewModel viewModelToValidate)
where TController : ApiController
{
var validationContext = new ValidationContext(viewModelToValidate, null, null);
var validationResults = new List<ValidationResult>();
Validator.TryValidateObject(viewModelToValidate, validationContext, validationResults, true);
foreach (var validationResult in validationResults)
{
controller.ModelState.AddModelError(validationResult.MemberNames.FirstOrDefault() ?? string.Empty, validationResult.ErrorMessage);
}
}
上記の回答編集を参照してください...