2015-06-05 13 views
7

Sto cercando di scrivere test unitari per convalidare il mio controller assicurandomi che le proprietà di binding siano impostate correttamente. Con la seguente struttura del metodo, come posso garantire che solo i campi validi siano passati da un test unitario?Unit Test the BindAttribute per i parametri del metodo

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

più ampia Spiegazione: Molti dei nostri modelli sono elenchi di valori consentiti che non vogliamo inviare avanti e indietro, così noi li ricostruire quando il (ModelState.IsValid == false). Al fine di garantire che tutto funzioni, vogliamo mettere in atto dei test unitari per affermare che l'elenco è stato ricostruito, ma senza cancellare l'elenco prima di chiamare il metodo, il test non è valido.

Stiamo utilizzando il metodo di supporto da questo SO answer per garantire che il modello sia convalidato, e quindi il nostro test dell'unità è simile a questo.

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

Qualsiasi assistenza o puntatori nella giusta direzione è molto apprezzata.

+0

E 'un po' non chiaro cosa stai cercando. Stai cercando un modo per rilevare che l'attributo Bind è stato utilizzato e impostato con i valori corretti sul tuo controller (ID, Foo ...)? O stai cercando un modo per verificare che il runtime MVC utilizzi correttamente l'attributo? Oppure un modo per applicare manualmente l'attributo al modello di test per ricreare il comportamento del runtime MVC, in modo da poter testare i metodi? O qualcos'altro interamente? – forsvarir

+0

Sto cercando un modo per verificare che l'attributo bind sia applicato, in modo che se un modello ha campi che non sono dichiarati per l'associazione, i valori non vengono passati dal test al controller nello stesso modo in cui non sarebbero passati dalla vista al controller su un post. L'obiettivo finale è garantire che tutti i campi siano collegati correttamente con i soli valori associati aggiornati su un post al controller, oltre a poter pubblicare un modello "cattivo" (accade per alcuni casi di convalida del server) e avere la logica per se (ModelState.IsValid) esercitata da un test. –

+1

È fantastico che tu abbia funzionato. Generalmente non è una buona idea modificare la tua soluzione nella tua domanda, dal momento che è effettivamente una risposta, piuttosto che una domanda. Posso suggerire che potresti voler modificare la tua domanda e postarla invece come auto-risposta alla domanda. Aiuta a partizionare il Q & A e come bonus si potrebbe finire con l'upvote dispari per questo. Se vuoi mantenere la tua soluzione accanto alla domanda quando le persone guardano il post, puoi anche cambiare la risposta accettata al tuo post (che non ho problemi anche se le opinioni di altre persone differiscono) – forsvarir

risposta

2

Posso interpretare erroneamente ciò che stai dicendo, ma sembra che tu voglia qualcosa per garantire che un modello che hai creato nel test venga filtrato prima di essere passato al controller per simulare il binding MVC e per evitare di scrivere accidentalmente un test che trasmette informazioni al controller in prova che non sarebbe mai stato effettivamente popolato dal framework.

Con questo in mente, ho dato per scontato che tu sia interessato solo agli attributi Bind con il set di membri Include. Nel qual caso si potrebbe usare qualcosa di simile:

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

Che così com'è sarebbe stato chiamato dal test di unità, prima di richiamare l'azione di controllo in questo modo:

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

In sostanza, ciò che fa è iterato attraverso ciascuno dei parametri che vengono passati a un determinato metodo di controller, cercando gli attributi di bind. Se trova un attributo bind, allora ottiene l'elenco Include, quindi reimposta tutte le proprietà dello viewModel che non è menzionato nell'elenco di inclusione (essenzialmente lo slegamento).

Il codice sopra riportato potrebbe richiedere qualche ritocco, non lavoro molto su MVC, quindi ho fatto alcune ipotesi sull'utilizzo dell'attributo e dei modelli.

Una versione migliorata del codice di cui sopra, che utilizza la BindAttribute stesso per fare il filtraggio:

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

Questo è vicino a quello che sto cercando. Speravo che esistesse una funzionalità incorporata in MVC per i test che mi permettesse di attivare il codice di bind effettivo anziché emularlo. Nel peggiore dei casi, posso seguire questa strada se necessario. –

+0

@MartinNoreke Ho migliorato il codice un po 'per usare BindAttribute per fare il filtraggio, anche se presuppone ancora che il modello sia popolato, quindi elimina i valori che non dovrebbero esserci. Penso che con il runtime ci si avvicini molto di più, è possibile che tu debba iniziare a invocare effettivamente la logica di binding con un contesto di controller e un provider di valori. Se non ottieni quello che cerchi, allora potresti voler scavare attorno al sorgente MVC: https://aspnetwebstack.codeplex.com/ – forsvarir

+0

Inizierò a scavare in questo in un giorno o due di nuovo. Sono stato portato avanti su un altro lavoro (che non succede mai :), ma penso che questo mi porti nella giusta direzione. –

1

Basato sulla risposta fornita da Forsvarir, mi si avvicinò con questo come la mia implementazione finale. Ho rimosso i generici per ridurre la digitazione ogni volta che è stato utilizzato e lo ho inserito in una classe base dei miei test. Ho anche dovuto fare del lavoro extra per più metodi con lo stesso nome ma diversi parametri (es: Get vs. Post) che è stato risolto dal ciclo di tutti i metodi invece di 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; 
      } 
     } 
    } 

Nel complesso questo è diventato una soluzione molto pulita e semplice, così ora il mio test simile a questa:

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

Solo per riferimento, il mio metodo ValidateViewModel è:

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