web-dev-qa-db-ja.com

MVC検証の単体テスト

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検証をテストしていますが、この方法でテストする必要がありますか?

75
Matthew Groves

古い投稿をネクロするのは嫌いですが、私は自分の考えを追加したいと思いました(この問題があり、答えを探している間にこの投稿に出くわしたからです)。

  1. コントローラのテストで検証をテストしないでください。 MVCの検証を信頼するか、独自のコードを記述します(つまり、他のコードをテストせず、コードをテストします)
  2. 検証が期待どおりに実行されていることをテストする場合は、モデルテストでテストします(これは、より複雑な正規表現の検証のために行います)。

ここで本当にテストしたいのは、検証が失敗したときにコントローラーが期待することを実行することです。それがあなたのコードであり、あなたの期待です。テストが必要なのはこれだけだとわかったら、テストは簡単です。

[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));
}
192
ARM

私は同じ問題を抱えていましたが、ポールの回答とコメントを読んだ後、ビューモデルを手動で検証する方法を探しました。

このチュートリアル を見つけました。これは、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);
        }
    }

これまでのところ、これは非常にうまく機能しています。

87
Giles Smith

テストでhomeController.Indexメソッドを呼び出すときは、検証を実行するMVCフレームワークを使用していないため、ModelState.IsValidは常にtrueになります。このコードでは、アンビエント検証を使用するのではなく、コントローラーでヘルパーValidateメソッドを直接呼び出します。 DataAnnotations(私たちはNHibernate.Validatorsを使用しています)の経験はあまりありませんが、コントローラー内からValidateを呼び出す方法について他の誰かがガイダンスを提供できるかもしれません。

7
Paul Alexander

今日私はこれを調査していたのですが、ロバート・ヘルナンデス(MVP)による このブログ投稿 が見つかりました。これにより、エンティティの検証時にModelStateに正しいエラーが記録されます。

3
Darren

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

検証については気にするが、実装方法については気にしない場合、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();
1

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フレームワークによって自動的に行われるのではなく、明示的に行われるため、コントローラーはテスト可能になります。

1
codeulike

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);
}
1
Alex York

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

このセットアップの利点は、すべてのモデルのテストにテストコントローラーを再利用でき、コントローラーについてもう少しモックを作成したり、コントローラーが持つ保護されたメソッドを使用したりできることです。

それが役に立てば幸い。

1
Albert

@ 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);
        }
    }
0
Sam Shiles

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

上記の回答編集を参照してください...

0
malix