2015-06-05 10 views
7

szukam do pisania testów jednostkowych do sprawdzania poprawności mojego kontrolera, zapewniając jednocześnie, że właściwości wiązania są ustawione prawidłowo. Z następującą strukturą metody, jak mogę zapewnić, że tylko poprawne pola są przekazywane z testu jednostkowego?test unit BindAttribute dla metody Parametry

public ActionResult AddItem([Bind(Include = "ID, Name, Foo, Bar")] ItemViewModel itemData) 
{ 
    if (ModelState.IsValid) 
    { 
     // Save and redirect 
    } 

    // Set Error Messages 
    // Rebuild object drop downs, etc. 
    itemData.AllowedFooValues = new List<Foo>(); 
    return View(itemData); 
} 

Szersze Objaśnienie: Wiele z naszych modeli mają wykazy dozwolonych wartości, które nie chcą, aby wysłać tę iz powrotem, więc je odbudować, gdy (ModelState.IsValid == false). W celu zapewnienia te wszystkie prace, chcemy umieścić jednostki badań w celu stwierdzenia, że ​​lista została odbudowana, ale bez kasowania listę przed wywołaniem metody, test jest nieważny.

Używamy metody pomocniczej z tej SO answer, aby zapewnić sprawdzenie modelu, a następnie nasz test jednostki jest podobny do tego.

public void MyTest() 
    { 
     MyController controller = new MyController(); 

     ActionResult result = controller.AddItem(); 
     Assert.IsNotNull(result); 
     ViewResult viewResult = result as ViewResult; 
     Assert.IsNotNull(viewResult); 
     ItemViewModel itemData = viewResult.Model as ItemViewModel; 
     Assert.IsNotNull(recipe); 
     // Validate model, will fail due to null name 
     controller.ValidateViewModel<ItemViewModel, MyController>(itemData); 

     // Call controller action 
     result = controller.AddItem(itemData); 
     Assert.IsNotNull(result); 
     viewResult = result as ViewResult; 
     Assert.IsNotNull(viewResult); 
     itemData = viewResult.Model as ItemViewModel; 
     // Ensure list was rebuilt 
     Assert.IsNotNull(itemData.AllowedFooValues); 
    } 

Każda pomoc lub wskazówki we właściwym kierunku są mile widziane.

+0

To trochę niejasne, czego szukasz. Czy szukasz sposobu na wykrycie, że atrybut Bind został użyty i skonfigurowany z poprawnymi wartościami kontrolera (ID, Foo ...)? A może szukasz sposobu na sprawdzenie, czy środowisko wykonawcze MVC poprawnie używa tego atrybutu? Lub sposób ręcznego zastosowania atrybutu do modelu testowego w celu odtworzenia zachowania środowiska wykonawczego MVC, aby można było przetestować swoje metody? Lub coś zupełnie innego? – forsvarir

+0

Szukam sposobu na sprawdzenie, czy atrybut wiązania jest zastosowany, więc jeśli model ma pola, które nie są zadeklarowane do wiązania, wartości nie są przekazywane z testu do kontrolera w taki sam sposób, w jaki nie zostałyby przekazane z widoku do kontrolera na poczcie. Ostatecznym celem jest upewnienie się, że wszystkie pola są prawidłowo powiązane z aktualizacją tylko powiązanych wartości na stanowisku do kontrolera, a także możliwość opublikowania "złego" modelu (dzieje się to w przypadku niektórych przypadków sprawdzania poprawności serwera) i logika, jeśli (ModelState.IsValid) wykonywane przez test. –

+1

To świetnie, że to działa. Na ogół nie jest dobrym pomysłem edytowanie twojego rozwiązania na twoje pytanie, ponieważ jest to skuteczna odpowiedź, a nie pytanie. Czy mogę zasugerować, że możesz go edytować z pytania i opublikować zamiast tego jako samodzielną odpowiedź na pytanie. Pomaga to w podzieleniu się pytaniami i odpowiedziami, a jako bonus możesz otrzymać dziwny upvote. Jeśli chcesz zachować rozwiązanie obok pytania, gdy ludzie patrzą na post, możesz też przełączyć zaakceptowaną odpowiedź na swój wpis (z którym nie mam problemu, chociaż widoki innych ludzi różnią się). – forsvarir

Odpowiedz

2

Mogę źle interpretować to, co mówisz, ale brzmi to tak, jakbyś chciał czegoś, aby upewnić się, że model utworzony w teście jest filtrowany przed przekazaniem go do kontrolera, aby symulować wiązanie MVC i zapobiec przypadkowemu napisaniu testu, który przekazuje informacje do testowanego kontrolera, który nigdy nie zostałby zapełniony przez framework.

Mając to na uwadze, mam przyjąć, że jesteś naprawdę zainteresowany tylko w Bind atrybutów ze zbioru Include członkowskim. W takim przypadku można użyć coś takiego:

public static void PreBindModel<TViewModel, TController>(this TController controller, 
                 TViewModel viewModel, 
                 string operationName) { 
    foreach (var paramToAction in typeof(TController).GetMethod(operationName).GetParameters()) { 
     foreach (var bindAttribute in paramToAction.CustomAttributes.Where(x => x.AttributeType == typeof(BindAttribute))) { 
      string properties; 
      try { 
       properties = bindAttribute.NamedArguments.Where(x => x.MemberName == "Include").First().TypedValue.Value.ToString(); 
      } 
      catch (InvalidOperationException) { 
       continue; 
      } 
      var propertyNames = properties.Split(','); 

      var propertiesToReset = typeof(TViewModel).GetProperties().Where(x => propertyNames.Contains(x.Name) == false); 

      foreach (var propertyToReset in propertiesToReset) { 
       propertyToReset.SetValue(viewModel, null); 
      } 
     } 
    } 
} 

które w obecnej formie będzie nazywany od badanej jednostki, zanim wywołać działanie kontrolera takiego:

controllerToTest.PreBindModel(model, "SomeMethod"); 
var result = controllerToTest.SomeMethod(model); 

Zasadniczo, co robi jest iteracyjne poprzez każdy z parametrów, które są przekazywane do danej metody kontrolera, szukając atrybutów wiązania. Jeśli stwierdzi atrybut powiązania, to dostaje listę Include, to zeruje każdą własność viewModel który nie został wymieniony w wykazie obejmują (zasadniczo Rozpinanie go).

Powyższy kod może wymagać trochę szczypanie, nie robię dużo MVC pracy, więc zrobiłem kilka założeń o wykorzystaniu atrybutu i modeli.

Ulepszona wersja powyższego kodu, który używa samego BindAttribute zrobić filtrowanie:

public static void PreBindModel<TViewModel, TController>(this TController controller, TViewModel viewModel, string operationName) { 
    foreach (var paramToAction in typeof(TController).GetMethod(operationName).GetParameters()) { 
     foreach (BindAttribute bindAttribute in paramToAction.GetCustomAttributes(true)) {//.Where(x => x.AttributeType == typeof(BindAttribute))) { 
      var propertiesToReset = typeof(TViewModel).GetProperties().Where(x => bindAttribute.IsPropertyAllowed(x.Name) == false); 

      foreach (var propertyToReset in propertiesToReset) { 
       propertyToReset.SetValue(viewModel, null); 
      } 
     } 
    } 
} 
+0

Jest to zbliżone do tego, czego szukam. Miałem nadzieję, że w MVC jest wbudowana funkcja do testowania, która umożliwi mi wywołanie rzeczywistego kodu powiązania zamiast emulować go. Najgorszy scenariusz W razie potrzeby mogę przejść tą ścieżką. –

+0

@MartinNoreke Ulepszyłem trochę kod, aby użyć funkcji BindAttribute do filtrowania, chociaż nadal zakłada się, że model jest wypełniany, a następnie usuwa wartości, których nie powinno tam być. Myślę, że aby uzyskać znacznie bliżej niż w środowisku wykonawczym, może się okazać, że trzeba zacząć wywoływać logikę wiązania z kontekstem kontrolera i dostawcą wartości. Jeśli nie dostaniesz tego, czego szukasz, możesz chcieć wykopać się wokół źródła MVC: https://aspnetwebstack.codeplex.com/ – forsvarir

+0

Zacznę zagłębiać się w to za dzień lub dwa ponownie. Zrobiłem sobie przerwę w innej pracy (to się nigdy nie zdarza :), ale myślę, że to prowadzi mnie we właściwym kierunku. –

1

Na podstawie odpowiedzi udzielonej przez Forsvarir, wpadłem na to jak mój ostatecznego wdrożenia. Usunąłem generyczne, aby zmniejszyć pisanie za każdym razem, gdy był używany, i umieścić to w klasie bazowej moich testów. Musiałem również wykonać dodatkową pracę dla wielu metod o tej samej nazwie, ale o różnych parametrach (np. Get vs. Post), które zostały rozwiązane przez pętlę wszystkich metod zamiast GetMethod.

public static void PreBindModel(Controller controller, ViewModelBase viewModel, string operationName) 
    { 
     MethodInfo[] methods = controller.GetType().GetMethods(); 
     foreach (MethodInfo currentMethod in methods) 
     { 
      if (currentMethod.Name.Equals(operationName)) 
      { 
       bool foundParamAttribute = false; 
       foreach (ParameterInfo paramToAction in currentMethod.GetParameters()) 
       { 
        object[] attributes = paramToAction.GetCustomAttributes(true); 
        foreach (object currentAttribute in attributes) 
        { 
         BindAttribute bindAttribute = currentAttribute as BindAttribute; 
         if (bindAttribute == null) 
          continue; 

         PropertyInfo[] allProperties = viewModel.GetType().GetProperties(); 
         IEnumerable<PropertyInfo> propertiesToReset = 
          allProperties.Where(x => bindAttribute.IsPropertyAllowed(x.Name) == false); 

         foreach (PropertyInfo propertyToReset in propertiesToReset) 
         { 
          propertyToReset.SetValue(viewModel, null); 
         } 

         foundParamAttribute = true; 
        } 
       } 

       if (foundParamAttribute) 
        return; 
      } 
     } 
    } 

Ogólnie ten stał się bardzo czyste i proste rozwiązanie, więc teraz moje testy wyglądać następująco:

[TestMethod] 
public void MyTest() 
{ 
    MyController controller = new MyController(); 

    ActionResult result = controller.MyAddMethod(); 
    Assert.IsNotNull(result); 
    ViewResult viewResult = result as ViewResult; 
    Assert.IsNotNull(viewResult); 
    MyDataType myDataObject = viewResult.Model as MyDataType; 
    Assert.IsNotNull(myDataObject); 
    ValidateViewModel(myController, myDataObject); 
    PreBindModel(controller, myDataObject, "MyAddMethod"); 
    Assert.IsNull(myDataObject.FieldThatShouldBeReset); 
    result = controller.MyAddMethod(myDataObject); 
    Assert.IsNotNull(result); 
    viewResult = result as ViewResult; 
    Assert.IsNotNull(viewResult); 
    myDataObject = viewResult.Model as MyDataType; 
    Assert.IsNotNull(myDataObject.FieldThatShouldBeReset); 
} 

tylko w celach informacyjnych, moja metoda ValidateViewModel jest:

public static void ValidateViewModel(BaseAuthorizedController controller, ViewModelBase 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); 
     } 
    }