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


91

Я проверяю 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, как показано в моем ответе.
Дарин Димитров

ИМХО Лучшее решение - использовать конвейер 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 07

Похоже на отличное решение, еще в 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 во входной поток вашего запроса (обратите внимание, поскольку этот входной поток является потоком памяти, можно оставить его нераспределенным, как память stream не привязан ни к каким внешним ресурсам):

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.