Модульные тесты на проверку MVC

Как я могу проверить, что мое действие контроллера помещает правильные ошибки в ModelState при проверке объекта, когда я использую валидацию DataAnnotation в MVC 2 Preview 1?

Некоторый код для иллюстрации. Во-первых, действие:

    [HttpPost]
    public ActionResult Index(BlogPost b)
    {
        if(ModelState.IsValid)
        {
            _blogService.Insert(b);
            return(View("Success", b));
        }
        return View(b);
    }

И вот неудачный unit test, который, я думаю, должен проходить, но не работает (используя 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);
}

Думаю, в дополнение к этому вопросу, следует ли тестировать проверку, и должен ли я тестировать ее таким образом?

Ответ 1

Вместо передачи в 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);
    }

Просто убедитесь, что ваш тест добавляет нулевое значение для каждого поля формы вида, которое вы хотите оставить пустым.

Я обнаружил, что делать это таким образом, за счет нескольких дополнительных строк кода, делает мои модульные тесты похожими на то, как код становится вызываемым во время выполнения, что делает их более ценными. Также вы можете проверить, что происходит, когда кто-то вводит "abc" в элемент управления, привязанный к свойству int.

Ответ 2

Ненависть к некрому старый пост, но я подумал, что добавлю свои собственные мысли (так как у меня была эта проблема и я столкнулся с этим сообщением при поиске ответа).

  • Не проверяйте проверку в тестах контроллера. Либо вы доверяете проверке MVC, либо пишете свою собственную (т.е. Не проверяете другой код, проверяете свой код).
  • Если вы хотите проверить правильность проверки, выполняйте то, что ожидаете, проверьте ее в своих тестовых тестах (я делаю это для нескольких моих более сложных регулярных выражений).

Что вы действительно хотите проверить здесь, так это то, что ваш контроллер делает то, что вы ожидаете от него, когда проверка не выполняется. Это ваш код и ваши ожидания. Тестирование легко, если вы осознаете, что все, что вы хотите проверить:

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

Ответ 3

У меня была такая же проблема, и после прочтения ответа и комментариев Pauls я искал способ ручной проверки модели представления.

Я нашел этот учебник, в котором объясняется, как вручную проверять ViewModel, который использует DataAnnotations. Они Key code фрагмент находится в конце сообщения.

Я слегка изменил код - в учебнике 4-й параметр TryValidateObject опущен (validateAllProperties). Чтобы получить все аннотации к Validate, это должно быть установлено в 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);
        }
    }

До сих пор это работало очень хорошо для нас.

Ответ 4

Когда вы вызываете метод homeController.Index в своем тесте, вы не используете ни одну из фреймворков MVC, которая отключает проверку, поэтому ModelState.IsValid всегда будет правдой. В нашем коде мы вызываем метод helper Validate непосредственно в контроллере, а не используя аттестацию. У меня не было большого опыта работы с DataAnnotations (мы используем NHibernate.Validators), возможно, кто-то еще может предложить руководство по вызову Validate из вашего контроллера.

Ответ 5

Я изучал это сегодня, и я нашел этот пост в блоге от Роберто Эрнандеса (MVP), который, кажется, обеспечивает лучшее решение для запуска валидаторов для действия контроллера во время модульного тестирования. Это приведет к правильным ошибкам в ModelState при проверке сущности.

Ответ 6

Я использую ModelBinders в своих тестовых случаях, чтобы иметь возможность обновить model.IsValid значение.

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

Ответ 7

Это не отвечает на ваш вопрос, потому что он отказывается от DataAnnotations, но я добавлю его, потому что он может помочь другим людям написать тесты для своих контроллеров:

У вас есть возможность не использовать проверку, предоставленную System.ComponentModel.DataAnnotations, но все же используя объект ViewData.ModelState, используя его метод AddModelError и другой механизм проверки. Например:

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

Это все еще позволяет вам использовать материал Html.ValidationMessageFor(), генерируемый MVC, без использования DataAnnotations. Вы должны убедиться, что ключ, который вы используете с AddModelError, соответствует ожидаемому ожиданию сообщений проверки.

Контроллер затем становится проверяемым, потому что проверка выполняется явно, а не выполняется автоматически с помощью структуры MVC.

Ответ 8

Я согласен с тем, что у ARM есть лучший ответ: проверьте поведение вашего контроллера, а не встроенную проверку.

Однако вы также можете unit test, чтобы ваша модель /ViewModel имела правильные атрибуты проверки. Скажем, ваш ViewModel выглядит так:

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

Этот unit test будет проверять наличие атрибута [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);
}

Ответ 9

В отличие от ARM, у меня нет проблем с серьезным рытьем. Итак, вот мое предложение. Он основывается на ответе Джайлса Смита и работает в 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.");
    }

Преимущество этой установки заключается в том, что я могу повторно использовать тестовый контроллер для тестирования всех своих моделей и, возможно, смогу расширить его, чтобы немного высмеять информацию о контроллере или использовать защищенные методы, которые имеет контроллер.

Надеюсь, что это поможет.

Ответ 10

Если вы заботитесь о проверке, но вам не важно, как это реализовано, если вы только заботитесь о проверке вашего метода действий на самом высоком уровне абстракции, независимо от того, реализовано ли оно как использование 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();

Ответ 11

На основе ответов и комментариев @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);
        }
    }

См. раздел "Редактирование ответов" выше...

Ответ 12

@Ответ на 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);
        }
    }