Модульні тести з валідації MVC


77

Як я можу перевірити, що моя дія контролера вводить правильні помилки в 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);
}

Я думаю, на додаток до цього питання, чи варто мені перевіряти перевірку та чи слід перевіряти це таким чином?


5
Чи не var p = new BlogPost {Title = "test"}; більше організувати, ніж діяти?
RichardOD

1
Assert.IsFalse (homeController.ModelState.IsValid);
Seth Flowers

Відповіді:


-4

Замість передачі a 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
Мені подобається такий підхід, але він здається кроком назад або, принаймні, одним додатковим кроком, який я повинен робити в кожній дії, яка обробляє POST.
Метью Гроувс

2
Я згоден. Але те, щоб мої модульні тести та реальний додаток працювали однаково, вартує зусиль.
Моріс

5
ARMs підхід кращий IMHO :)
kamranicus

4
Цей вид перемагає мету MVC.
Енді,

2
Я згоден, що відповідь ARM є кращою. Передача у FormCollection дії контролера небажана порівняно з передачею сильно набраного об'єкта Model / ViewModel.
Алекс Йорк,

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

2
Я згоден, це повинна бути правильна відповідь. Як говорить ARM: вбудовану перевірку не слід перевіряти. Натомість поведінку вашого контролера слід перевіряти. Це має найбільший сенс.
Алекс Йорк,

Контролер слід тестувати окремо від прив'язки та перевірки моделі. Слідує як KISS, так і відокремлення проблем. Я роблю невеличку серію статей про модульне
TiMoch

3
Що потрібно зробити, щоб протестувати власні атрибути перевірки? Якщо вони використовуються, тоді не можна "довіряти валідації MVC". Як би ви перевірили (мабуть, у тестах моделі), чи працює спеціальна перевірка?
Джон Сондерс,

2
Я не згоден. Нам ще потрібно перевірити, що дана модель буде спричиняти помилки моделі, що використовуються як передумова у цьому тесті. Приклад коду, однак, є ідеальною відповіддю на власне визначене питання в 1. Однак це не відповідь на початкове запитання
Ібрагім бен Салах

Це не перевірка перевірки моделі. Приміром, хтось міг (навмисно чи випадково) видалити анотацію даних у моделі (можливо, помилку злиття?), І цей тест не провалиться.
Росді Касім

89

У мене була та ж проблема, і після прочитання відповіді та коментаря Паулса я шукав спосіб ручної перевірки моделі подання.

Я знайшов цей підручник, який пояснює, як вручну перевірити ViewModel, який використовує DataAnnotations. Фрагмент коду Key Key знаходиться в кінці допису.

Я трохи змінив код - у підручнику 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);
        }
    }

Наразі це у нас працювало дуже добре.


Вибачте, навіть не перевірив цього. Всі наші проекти MVC знаходяться в 4.0
Giles Smith

Дякую за це! Невеликий додаток; якщо у вас є перевірка, яка не пов'язана з певним полем (тобто ви реалізували IValidatableObject), MemberNames порожній, а ключ помилки моделі повинен бути порожнім рядком. У foreach ви можете зробити: var key = validationResult.MemberNames.Any() ? validationResult.MemberNames.First() : string.Empty; controller.ModelState.AddModelError(key, validationResult.ErrorMessage);
Thomas Lundström

6
Чому для цього потрібно використовувати Generics? Це можна було б використовувати набагато простіше, якби це було визначено як: void ValidateViewModel (object viewModelToValidate, контролер контролера) або навіть краще як метод розширення: public static void ValidateViewModel (this Controller controller, object viewModelToValidate)
Chad Grant

Це чудово, але я погоджуюсь з Чадом, просто позбудьтесь загального синтаксису.
Роджер,

Якщо хтось мав таку ж проблему, як і я, з "Валідатором", використовуйте "System.ComponentModel.DataAnnotations.Validator.TryValidateObject", щоб переконатися, що Ви використовуєте правильний Валідатор.
Алін Чокан

7

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


Мені подобається термін "перевірка навколишнього середовища". Але все-таки повинен бути спосіб викликати це в юніт-тесті?
Метью Гроувс

3
Проблема в тому, що ви в основному тестуєте фреймворк MVC, а не ваш контролер. Ви намагаєтесь підтвердити, що MVC перевіряє вашу модель, як ви очікували. Єдиним способом зробити це з будь-якою впевненістю було б знущатись над всім конвеєром MVC та моделювати веб-запит. Це, мабуть, більше, ніж вам дійсно потрібно знати. Якщо ви просто перевіряєте, чи перевірка даних на ваших моделях налаштована правильно, ви можете зробити це без контролера і просто запустити перевірку даних вручну.
Пол Олександр

3

Я досліджував це сьогодні і знайшов цю публікацію в блозі Роберто Ернандеса (MVP), яка, мабуть, забезпечує найкраще рішення для запуску валідаторів для дії контролера під час модульного тестування. Це призведе до правильних помилок у ModelState під час перевірки сутності.


2

У своїх тестових випадках я використовую 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);
        }

Це не працює, якщо у вас є більше одного атрибута перевірки для одного властивості. Додайте цей рядок controller.ModelState.Clear();перед кодом, який створює, ModelBindingContextі він буде працювати
Suhas

1

Це точно не відповідає на ваше запитання, оскільки воно відмовляється від 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.


Виконання перевірки таким чином відкидає деякі найкращі частини перевірки в MVC. Я хочу додати перевірку на свою модель, а не на контролер. Якщо я застосую це рішення, у мене з’явиться багато можливих дублікатів коду з супроводжуючими кошмарами.
Віллем Мейнтс

@ W.Meints: вірно, але рядки коду у наведеному вище прикладі, які виконують перевірку, також можуть бути переміщені до методу на Моделі, якщо вам подобається. Справа в тому, що перевірка за допомогою коду, а не атрибутів, робить її більш перевіряемою. Павло пояснює , що краще вище stackoverflow.com/a/1269960/22194
codeulike

1

Я погоджуюсь, що 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);
}

Як ми тоді перевіримо вбудовану перевірку? Особливо, якщо ми налаштували його за допомогою додаткових атрибутів, повідомлень про помилки тощо
Teoman shipahi

1

На відміну від 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.");
    }

Перевага цієї установки полягає в тому, що я можу повторно використовувати контрольний контролер для тестів усіх своїх моделей і, можливо, зможу розширити його, щоб трохи більше знущатися про контролер або використовувати захищені методи, які має контролер.

Сподіваюся, це допоможе.


1

Якщо ви дбаєте про перевірку, але вам байдуже про те, як вона реалізована, якщо вам важлива лише перевірка вашого методу дії на найвищому рівні абстракції, незалежно від того, реалізована вона як за допомогою 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();

0

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

Дивіться у редагуванні відповіді вище ...


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);
        }
    }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.