Jak mock ModelState.IsValid przy użyciu struktury Moq?


91

Sprawdzam ModelState.IsValidmetodę akcji kontrolera, która tworzy pracownika w następujący sposób:

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

    // Etc.
}

Chcę go wyśmiewać w mojej metodzie testów jednostkowych przy użyciu Moq Framework. Próbowałem to kpić w ten sposób:

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

Ale to zgłasza wyjątek w moim przypadku testu jednostkowego. Czy ktoś może mi tu pomóc?

Odpowiedzi:


142

Nie musisz z tego kpić. Jeśli masz już kontroler, możesz dodać błąd stanu modelu podczas inicjowania testu:

// 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();

jak ustawić ModelState.IsValid, aby trafił w prawdziwy przypadek? ModelState nie ma metody ustawiającej, dlatego nie możemy wykonać następujących czynności: _controllerUnderTest.ModelState.IsValid = true. Bez tego pracownika nie uderzy
Karan

4
@Newton, domyślnie jest to prawda. Nie musisz niczego określać, aby trafić w prawdziwy przypadek. Jeśli chcesz trafić w fałszywy przypadek, po prostu dodaj błąd stanu modelu, jak pokazano w mojej odpowiedzi.
Darin Dimitrov

IMHO Lepszym rozwiązaniem jest użycie przenośnika mvc. W ten sposób uzyskujesz bardziej realistyczne zachowanie swojego kontrolera, powinieneś dostarczyć walidację modelu do jego przeznaczenia - walidacje atrybutów. Poniższy post opisuje to ( stackoverflow.com/a/5580363/572612 )
Vladimir Shmidt

13

Jedyny problem z powyższym rozwiązaniem polega na tym, że w rzeczywistości nie testuje ono modelu, jeśli ustawię atrybuty. Skonfigurowałem kontroler w ten sposób.

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

Obiekt modelBinder jest obiektem testującym poprawność modelu. W ten sposób mogę po prostu ustawić wartości obiektu i przetestować go.


1
Bardzo ładnie, właśnie tego szukałem. Nie wiem, ile osób zadaje takie stare pytanie, ale miało to dla mnie jakąś wartość. Dzięki.
W.Jackson,

Wydaje się, że to świetne rozwiązanie, jeszcze w 2016 roku :)
Matt

2
Czy nie lepiej jest przetestować model w izolacji za pomocą czegoś takiego? stackoverflow.com/a/4331964/3198973
RubberDuck,

2
Chociaż jest to sprytne rozwiązanie, zgadzam się z @RubberDuck. Aby był to rzeczywisty, izolowany test jednostkowy, walidacja modelu powinna być jego własnym testem, podczas gdy testowanie kontrolera powinno mieć własne testy. Jeśli model zmieni się, aby naruszyć walidację ModelBinder, test kontrolera zakończy się niepowodzeniem, co jest fałszywie dodatnim wynikiem, ponieważ logika kontrolera nie jest zepsuta. Aby przetestować nieprawidłowy ModelStateDictionary, po prostu dodaj fałszywy błąd ModelState, aby sprawdzanie ModelState.IsValid zakończyło się niepowodzeniem.
xDaevax,

2

Odpowiedź uadrive zajęła mi część drogi, ale nadal były pewne luki. Bez żadnych danych wejściowych do new NameValueCollectionValueProvider(), spinacz modelu powiąże kontroler z pustym modelem, a nie z modelobiektem.

W porządku - po prostu serializuj swój model jako a NameValueCollection, a następnie przekaż go do NameValueCollectionValueProviderkonstruktora. Cóż, niezupełnie. Niestety w moim przypadku to nie zadziałało, ponieważ mój model zawiera kolekcję i NameValueCollectionValueProvidernie bawi się ładnie z kolekcjami.

Tutaj jednak JsonValueProviderFactoryprzychodzi z pomocą. Może być używany przez, o DefaultModelBinderile określisz typ zawartości "application/json„i przekażesz zserializowany obiekt JSON do strumienia wejściowego żądania (Uwaga, ponieważ ten strumień wejściowy jest strumieniem pamięci, można go pozostawić niewykorzystany jako pamięć stream nie zatrzymuje żadnych zasobów zewnętrznych):

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;
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.