Esiste un valore reale nell'unità che verifica un controller in ASP.NET MVC?

32

Spero che questa domanda dia alcune risposte interessanti perché è una di quelle che mi hanno infastidito per un po '.

Esiste un valore reale nell'unità di test di un controller in ASP.NET MVC?

Ciò che intendo è, per la maggior parte del tempo, (e non sono un genio), i miei metodi di controllo sono, anche nella loro parte più complessa, qualcosa del genere:

public ActionResult Create(MyModel model)
{
    // start error list
    var errors = new List<string>();

    // check model state based on data annotations
    if(ModelState.IsValid)
    {
        // call a service method
        if(this._myService.CreateNew(model, Request.UserHostAddress, ref errors))
        {
            // all is well, data is saved, 
            // so tell the user they are brilliant
            return View("_Success");
        }
    }

    // add errors to model state
    errors.ForEach(e => ModelState.AddModelError("", e));

    // return view
    return View(model);
}

La maggior parte del sollevamento pesante viene eseguita dalla pipeline MVC o dalla mia libreria di servizi.

Quindi forse le domande potrebbero essere:

  • quale sarebbe il valore dell'unità testando questo metodo?
  • non si romperà su Request.UserHostAddress e ModelState con una NullReferenceException? Dovrei provare a deridere questi?
  • se rifaccio questo metodo in un "helper" riutilizzabile (che probabilmente dovrei, considerando quante volte lo faccio!), testerei che valga la pena anche quando tutto quello che sto testando è soprattutto la "pipeline" "che, presumibilmente, è stato testato per un pollice della sua vita da Microsoft?

Penso che il mio punto davvero sia, fare ciò che segue sembra assolutamente inutile e sbagliato

[TestMethod]
public void Test_Home_Index()
{
    var controller = new HomeController();
    var expected = "Index";
    var actual = ((ViewResult)controller.Index()).ViewName;
    Assert.AreEqual(expected, actual);
}

Ovviamente sono ottuso con questo esempio esageratamente inutile, ma qualcuno ha qualche sapienza da aggiungere qui?

Non vedo l'ora ... Grazie.

    
posta LiverpoolsNumber9 15.03.2013 - 17:01
fonte

3 risposte

18

Anche per qualcosa di così semplice, un test unitario avrà molteplici scopi

  1. Fiducia, ciò che è stato scritto è conforme all'output atteso. Può sembrare banale verificare che restituisca la vista corretta, ma il risultato è una prova oggettiva che il requisito è stato soddisfatto
  2. Test di regressione. Se il metodo Create deve essere modificato, è ancora disponibile un test unitario per l'output previsto. Sì, l'output potrebbe cambiare e questo si traduce in un test fragile, ma è ancora un controllo contro il controllo delle modifiche non gestite

Per quella particolare azione, testerei per il seguente

  1. Che cosa succede se _myService è null?
  2. Che cosa succede se _myService.Create genera un'eccezione, ne lancia di specifici da gestire?
  3. Un _myService.Create riuscito restituisce la vista _Success?
  4. Gli errori si propagano fino a ModelState?

Hai sottolineato il controllo di richiesta e modello per NullReferenceException e penso che ModelState.IsValid si occuperà della gestione di NullReference per il modello.

Deridere la richiesta consente di evitare una richiesta nulla che è generalmente impossibile in produzione, ma che può accadere in un test unitario. In un test di integrazione ti consentirebbe di fornire diversi valori di UserHostAddress (una richiesta è ancora l'input dell'utente per quanto riguarda il controllo e dovrebbe essere testata di conseguenza)

    
risposta data 15.03.2013 - 18:40
fonte
3

Anche i miei controller sono molto piccoli. La maggior parte della "logica" nei controller viene gestita utilizzando attributi di filtro (incorporati e scritti a mano). Quindi il mio controller di solito ha solo una manciata di posti di lavoro:

  • Crea modelli da stringhe di query HTTP, valori dei moduli, ecc.
  • Esegui alcune convalide di base
  • Chiama i miei dati o il mio livello aziendale
  • Genera un ActionResult

La maggior parte del collegamento del modello viene eseguita automaticamente da ASP.NET MVC. Anche DataAnnotation gestisce la maggior parte della validazione per me.

Anche con così poco da testare, in genere li scrivo. Fondamentalmente, provo che i miei repository sono chiamati e che viene restituito il tipo ActionResult corretto. Ho un metodo di convenienza per ViewResult per assicurarmi che venga restituito il percorso di visualizzazione corretto e che il modello di visualizzazione abbia l'aspetto che mi aspetto. Ne ho un'altra per controllare il giusto controller / l'azione è impostata per RedirectToActionResult . Ho altri test per JsonResult , ecc. Ecc.

Uno sfortunato risultato della sottoclassificazione della classe Controller è che fornisce molti metodi di convenienza che utilizzano internamente HttpContext . Ciò rende difficile testare il controller. Per questo motivo, di solito metto le chiamate di HttpContext -dipendenti dietro un'interfaccia e passo l'interfaccia al costruttore del controller (io uso l'estensione web di Ninject per creare i miei controller per me). Questa interfaccia è di solito in cui infilo le proprietà helper per accedere alla sessione, alle impostazioni di configurazione, agli IPrinciple e agli helper URL.

Questo richiede molta diligenza, ma penso che ne valga la pena.

    
risposta data 15.03.2013 - 18:08
fonte
2

Ovviamente alcuni controller sono molto più complessi di questo, ma si basano esclusivamente sul tuo esempio:

Che cosa succede se myService genera un'eccezione?

Come nota a margine.

Inoltre, metterei in dubbio la saggezza di passare una lista per riferimento (non è necessario dato che c # passa comunque per riferimento ma anche se non lo era) - passando un action Azione error (Azione) che il servizio può quindi utilizzare per pompare messaggi di errore a cui potrebbero poi essere gestiti come si desidera (forse si desidera aggiungerlo alla lista, magari si desidera aggiungere un errore del modello, magari si vuole loggarlo).

Nel tuo esempio:

invece di errori di ref, do (string s) = > ModelState.AddModelError ("", s) per esempio.

    
risposta data 15.03.2013 - 17:13
fonte

Leggi altre domande sui tag