Як знущатися над ModelState.IsValid за допомогою фреймворка Moq?


90

Я перевіряю ModelState.IsValidсвій метод дії контролера, який створює такого працівника:

[HttpPost]
public virtual ActionResult Create(EmployeeForm employeeForm)
{
    if (this.ModelState.IsValid)
    {
        IEmployee employee = this._uiFactoryInstance.Map(employeeForm);
        employee.Save();
    }

    // Etc.
}

Я хочу знущатися над цим у своєму методі модульного тестування за допомогою Moq Framework. Я намагався знущатись так:

var modelState = new Mock<ModelStateDictionary>();
modelState.Setup(m => m.IsValid).Returns(true);

Але це викликає виняток у моєму модульному тестовому випадку. Хтось може мені допомогти?

Відповіді:


142

Вам не потрібно знущатися над цим. Якщо у вас вже є контролер, ви можете додати помилку стану моделі під час ініціалізації тесту:

// arrange
_controllerUnderTest.ModelState.AddModelError("key", "error message");

// act
// Now call the controller action and it will 
// enter the (!ModelState.IsValid) condition
var actual = _controllerUnderTest.Index();

як ми встановлюємо ModelState.IsValid, щоб досягти справжнього випадку? ModelState не має сеттера, і, отже, ми не можемо зробити наступне: _controllerUnderTest.ModelState.IsValid = true. Без цього це не вдарить працівника
Каран

4
@Newton, це правда за замовчуванням. Вам не потрібно нічого вказувати, щоб отримати справжній випадок. Якщо ви хочете встановити помилковий випадок, ви просто додаєте помилку modelstate, як показано в моїй відповіді.
Дарін Димитров

IMHO Кращим рішенням є використання конвеєра mvc. Таким чином ви отримуєте більш реалістичну поведінку свого контролера, вам слід доставити перевірку моделі до її долі - перевірку атрибутів. Нижче допис описує це ( stackoverflow.com/a/5580363/572612 )
Володимир Шмідт

13

Єдине питання, яке виникає у мене із вищенаведеним рішенням, полягає в тому, що воно насправді не тестує модель, якщо я встановлюю атрибути. Я налаштував свій контролер таким чином.

private HomeController GenerateController(object model)
    {
        HomeController controller = new HomeController()
        {
            RoleService = new MockRoleService(),
            MembershipService = new MockMembershipService()
        };
        MvcMockHelpers.SetFakeAuthenticatedControllerContext(controller);

        // bind errors modelstate to the controller
        var modelBinder = new ModelBindingContext()
        {
            ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType()),
            ValueProvider = new NameValueCollectionValueProvider(new NameValueCollection(), CultureInfo.InvariantCulture)
        };
        var binder = new DefaultModelBinder().BindModel(new ControllerContext(), modelBinder);
        controller.ModelState.Clear();
        controller.ModelState.Merge(modelBinder.ModelState);
        return controller;
    }

Об'єкт modelBinder - це об'єкт, який перевіряє валідність моделі. Таким чином я можу просто встановити значення об'єкта і протестувати його.


1
Дуже приємно, це саме те, що я шукав. Не знаю, скільки людей розміщують старі запитання, подібні на це, але це було для вас цінним. Дякую.
W.Jackson

Здається, чудове рішення, все ще в 2016 році :)
Метт

2
Чи не краще протестувати модель ізольовано чимось подібним? stackoverflow.com/a/4331964/3198973
RubberDuck

2
Хоча це розумне рішення, я погоджуюсь з @RubberDuck. Щоб це було фактичним, ізольованим модульним тестом, перевірка моделі повинна бути власним тестом, тоді як тестування контролера повинно мати власні тести. Якщо модель зміниться, щоб порушити перевірку ModelBinder, тест вашого контролера не вдасться, що є помилково позитивним, оскільки логіка контролера не порушена. Щоб протестувати недійсний ModelStateDictionary, просто додайте помилкову помилку ModelState для перевірки ModelState.IsValid.
xDaevax

2

Відповідь uadrive зайняла мене частину шляху, але все-таки були певні прогалини. Без будь-яких даних на вході в new NameValueCollectionValueProvider(), прив’язка моделі прив’яже контролер до порожньої моделі, а не до modelоб’єкта.

Це чудово - просто серіалізуйте свою модель як NameValueCollection, а потім передайте її в NameValueCollectionValueProviderконструктор. Ну, не зовсім. На жаль, це не спрацювало у моєму випадку, оскільки моя модель містить колекцію, і NameValueCollectionValueProviderвона не дуже добре грає з колекціями.

Однак JsonValueProviderFactoryтут на допомогу приходить. Він може використовуватися до тих DefaultModelBinderпір, поки ви вкажете тип вмісту "application/json"і передасте ваш серіалізований об'єкт JSON у вхідний потік вашого запиту (Будь ласка, зверніть увагу, оскільки цей вхідний потік є потоком пам'яті, його можна залишити без використання, як пам'ять потік не тримається на жодних зовнішніх ресурсах):

protected void BindModel<TModel>(Controller controller, TModel viewModel)
{
    var controllerContext = SetUpControllerContext(controller, viewModel);
    var bindingContext = new ModelBindingContext
    {
        ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => viewModel, typeof(TModel)),
        ValueProvider = new JsonValueProviderFactory().GetValueProvider(controllerContext)
    };

    new DefaultModelBinder().BindModel(controller.ControllerContext, bindingContext);
    controller.ModelState.Clear();
    controller.ModelState.Merge(bindingContext.ModelState);
}

private static ControllerContext SetUpControllerContext<TModel>(Controller controller, TModel viewModel)
{
    var controllerContext = A.Fake<ControllerContext>();
    controller.ControllerContext = controllerContext;
    var json = new JavaScriptSerializer().Serialize(viewModel);
    A.CallTo(() => controllerContext.Controller).Returns(controller);
    A.CallTo(() => controllerContext.HttpContext.Request.InputStream).Returns(new MemoryStream(Encoding.UTF8.GetBytes(json)));
    A.CallTo(() => controllerContext.HttpContext.Request.ContentType).Returns("application/json");
    return controllerContext;
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.