78

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

この質問に加えて、検証をテストする必要があり、この方法でテストする必要があると思いますか?

4

12 に答える 12

194

古い投稿を壊すのは嫌いですが、自分の考えを追加したいと思いました(この問題が発生し、答えを探しているときにこの投稿に出くわしたためです)。

  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));
}
于 2010-09-28T19:02:35.903 に答える
91

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

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

これまでのところ、これは私たちにとって本当にうまくいきました。

于 2010-07-28T13:08:08.883 に答える
7

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

于 2009-08-13T03:58:12.283 に答える
3

今日これを調査していたところ、Roberto Hernández (MVP) によるこのブログ投稿を見つけました。これは、単体テスト中にコントローラー アクションのバリデーターを起動するための最良のソリューションを提供しているようです。これにより、エンティティの検証時に ModelState に正しいエラーが配置されます。

于 2010-10-05T03:45:24.927 に答える
2

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);
        }
于 2010-02-18T19:50:14.710 に答える
1

ARMとは対照的に、私は墓掘りに問題はありません。だからここに私の提案があります。これはGilesSmithの回答に基づいており、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);
            }
        }
    }

もちろん、クラスは保護された内部クラスである必要はありません。これが現在の使用方法ですが、おそらくそのクラスを再利用する予定です。素敵なデータアノテーション属性で装飾されたモデル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.");
    }

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

それが役に立てば幸い。

于 2013-03-21T20:38:10.200 に答える
1

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);
}
于 2012-05-26T16:41:41.143 に答える
1

検証は気にするが実装方法は気にしない場合、抽象化の最高レベルでのアクション メソッドの検証のみを気にする場合は、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();
于 2015-08-01T22:04:32.650 に答える
1

This doesn't exactly answer your question, because it abandons DataAnnotations, but I'll add it because it might help other people write tests for their Controllers:

You have the option of not using the validation provided by System.ComponentModel.DataAnnotations but still using the ViewData.ModelState object, by using its AddModelError method and some other validation mechanism. E.g:

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

This still lets you take advantage of the Html.ValidationMessageFor() stuff that MVC generates, without using the DataAnnotations. You have to make sure the key you use with AddModelError matches what the view is expecting for validation messages.

The controller then becomes testable because the validation is happening explicitly, rather than being done automagically by the MVC framework.

于 2010-09-23T19:52:22.573 に答える
0

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

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

于 2015-03-30T20:10:42.627 に答える
0

@ 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);
        }
    }
于 2017-02-09T08:42:24.523 に答える
-4

を渡す代わりにBlogPost、actions パラメータを として宣言することもできますFormCollection。次に、BlogPost自分自身を作成し​​て呼び出すことができますUpdateModel(model, formCollection.ToValueProvider());

これにより、 の任意のフィールドの検証がトリガーされますFormCollection

    [HttpPost]
    public ActionResult Index(FormCollection form)
    {
        var b = new BlogPost();
        TryUpdateModel(model, form.ToValueProvider());

        if (ModelState.IsValid)
        {
            _blogService.Insert(b);
            return (View("Success", b));
        }
        return View(b);
    }

空のままにしたいビュー フォームのすべてのフィールドに、テストで null 値が追加されていることを確認してください。

数行の余分なコードを犠牲にしてこのように行うと、単体テストが実行時にコードが呼び出される方法に似たものになり、より価値のあるものになることがわかりました。また、int プロパティにバインドされたコントロールに誰かが「abc」と入力するとどうなるかをテストすることもできます。

于 2009-08-13T07:32:09.027 に答える