Вопрос

Как я могу проверить, что мое действие контроллера помещает правильные ошибки в 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);
    }

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

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

Это было полезно?

Решение

Вместо того, чтобы передавать BlogPost вы также можете объявить параметр действий как 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.

Другие советы

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

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

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

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

Я немного подправил код — в туториале опущен 4-й параметр TryValidateObject (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 при проверке объекта.

Я использую 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);
        }

Это не совсем ответ на ваш вопрос, поскольку он отказывается от 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.

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

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

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

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

Если вас волнует проверка, но вас не волнует, как она реализована, если вас интересует только проверка вашего метода действия на самом высоком уровне абстракции, независимо от того, реализован ли он с использованием DataAnnotations, ModelBinders или даже ActionFilterAttributes, тогда вы можете использовать пакет nuget Xania.AspNet.Simulator следующим образом:

install-package Xania.AspNet.Simulator

--

var action = new BlogController()
    .Action(c => c.Index(new BlogPost()), "POST");
var modelState = action.ValidateRequest();

modelState.IsValid.Should().BeFalse();

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

См. редактирование ответа выше...

Ответ @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);
        }
    }
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top