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


33

Spero che questa domanda dia alcune risposte interessanti perché è quella che mi ha infastidito per un po '.

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

Quello che intendo con ciò è, il più delle volte (e non sono un genio), i miei metodi di controller sono, anche nella loro forma 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);
}

Gran parte del lavoro pesante viene svolto dalla pipeline MVC o dalla mia biblioteca di servizi.

Quindi forse le domande da porre potrebbero essere:

  • quale sarebbe il valore dell'unità test questo metodo?
  • non si spezzerebbe Request.UserHostAddresse ModelStatecon una NullReferenceException? Dovrei provare a deridere questi?
  • se rifrattassi questo metodo in un "aiutante" riutilizzabile (che probabilmente dovrei, considerando quante volte lo faccio!), testerei che valga la pena anche quando tutto ciò che sto davvero testando è principalmente la "pipeline" che, presumibilmente, è stato testato entro un centimetro dalla sua vita da Microsoft?

Penso che il mio punto sia davvero , fare quanto 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 saggezza da aggiungere qui?

Non vedo l'ora ... Grazie.


Penso che il ROI (Return on Investment) su quel particolare test non valga la pena, a meno che tu non abbia tempo e denaro infiniti. Scriverei dei test che Kevin sottolinea per controllare le cose che hanno maggiori probabilità di rompersi o che ti aiuteranno a refactoring qualcosa con fiducia o assicurando che la propagazione degli errori avvenga come previsto. I test della pipeline, se necessario, possono essere eseguiti a un livello più globale / infrastrutturale e a livello di singoli metodi avrà scarso valore. Non dire che non hanno valore, ma "piccolo". Quindi, se fornisce una buona ROI nel tuo caso, provaci, altrimenti, prima cattura il pesce più grande!
Mrchief,

Risposte:


18

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

  1. Fiducia, ciò che è stato scritto è conforme alla produzione prevista. 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 di creazione deve essere modificato, è ancora presente un test unitario per l'output previsto. Sì, l'output potrebbe cambiare lungo e ciò si traduce in un test fragile ma è comunque un controllo rispetto al controllo delle modifiche non gestito

Per quella particolare azione testerei per quanto segue

  1. Cosa succede se _myService è null?
  2. Cosa succede se _myService.Create genera un'eccezione, genera specifici da gestire?
  3. Un _myService.Create riuscito restituisce la vista _Success?
  4. Gli errori vengono propagati fino a ModelState?

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

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


Ciao Kevin, grazie per aver dedicato del tempo per rispondere. Lascerò un po 'per vedere se qualcun altro arriva con qualcosa, ma finora il tuo è il più logico / chiaro.
LiverpoolsNumber9

Spifty. Sono contento che ti abbia aiutato.
Kevin,

3

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

  • Crea modelli da stringhe di query HTTP, valori di modulo, ecc.
  • Eseguire una convalida di base
  • Chiama nei miei dati o livello aziendale
  • Genera a ActionResult

La maggior parte dell'associazione del modello viene eseguita automaticamente da ASP.NET MVC. DataAnnotations gestisce anche la maggior parte della convalida per me.

Anche con così poco da testare, in genere li scrivo ancora. Fondamentalmente, provo che i miei repository vengono chiamati e che ActionResultviene restituito il tipo corretto . Ho un metodo pratico per ViewResultassicurarmi che venga restituito il percorso di visualizzazione corretto e che il modello di visualizzazione sia come mi aspetto. Ne ho un altro per verificare che sia impostato il controller / azione corretti RedirectToActionResult. Ho altri test per JsonResult, ecc. Ecc.

Un risultato sfortunato della sottoclasse della Controllerclasse è che fornisce molti metodi di praticità che usano HttpContextinternamente. Ciò rende difficile testare l'unità di controllo. Per questo motivo, in genere inserisco HttpContextchiamate dipendenti da un'interfaccia e invio tale interfaccia al costruttore del controller (utilizzo l'estensione web Ninject per creare i miei controller per me). Questa interfaccia di solito è dove applico le proprietà dell'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.


Grazie per il tempo dedicato a rispondere, ma 2 problemi immediatamente. In primo luogo, i "metodi di supporto" nei test unitari sono v. Pericolosi. In secondo luogo, "prova che i miei repository sono chiamati" - intendi tramite iniezione di dipendenza?
LiverpoolsNumber9

Perché i metodi di convenienza sarebbero pericolosi? Ho una BaseControllerTestslezione in cui vivono tutti. Derido i miei repository. Li collego usando Ninject.
Travis Parks,

Cosa succede se hai commesso un errore o un'ipotesi errata nei tuoi aiutanti? L'altro punto era che solo un test di integrazione (cioè end-to-end) poteva "testare" se i tuoi repository sono chiamati. In un unit test verrai "nuovo" o deriso i tuoi archivi manualmente comunque.
LiverpoolsNumber9

Si passa il repository al costruttore. Lo prendi in giro durante il test. Ti assicuri che la derisione venga eseguita come previsto. Gli aiutanti semplicemente decostruiscono le ActionResults per ispezionare URL, modelli, ecc
Passati

Va bene, ho capito male cosa intendevi con "prova che i miei repository sono chiamati".
LiverpoolsNumber9

2

Ovviamente alcuni controller sono molto più complessi di quello ma basati esclusivamente sul tuo esempio:

Cosa succede se myService genera un'eccezione?

Come nota a margine.

Inoltre, metterei in dubbio la saggezza di passare un elenco per riferimento (non è necessario poiché c # passa comunque per riferimento ma anche se non lo fosse) - passare un errore Azione azione (Azione) che il servizio può quindi utilizzare per pompare i messaggi di errore a che potrebbe quindi essere gestito come desiderato (forse si desidera aggiungerlo all'elenco, forse si desidera aggiungere un errore del modello, forse si desidera registrarlo).

Nel tuo esempio:

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


Vale la pena ricordare che ciò presuppone che il servizio risieda nella stessa applicazione, altrimenti entreranno in gioco problemi di serializzazione.
Michael

Il servizio sarebbe in una DLL separata. Ma comunque, probabilmente hai ragione riguardo al "ref". D'altro canto, non importa se myService genera un'eccezione. Non sto testando myService: testerei i metodi ivi contenuti separatamente. Sto parlando di testare puramente l'unità "ActionResult" con (probabilmente) un beffardo myService.
LiverpoolsNumber9

Hai una mappatura 1: 1 tra il tuo servizio e il tuo controller? In caso contrario, alcuni controller utilizzano più chiamate di servizio? Se è così, potresti testare quelle interazioni?
Michael

No. Alla fine della giornata, i metodi di servizio prendono input (di solito un modello di vista o anche solo stringhe / ints), "fanno cose", quindi restituiscono un bool / errori se falso. Non esiste un collegamento "diretto" tra i controller e il livello di servizio. Sono completamente separati.
LiverpoolsNumber9

Sì, lo capisco, sto cercando di capire il modello relazionale tra i controller e il livello di servizio - supponendo che ciascun controller non abbia un metodo di servizio corrispondente, quindi sarebbe logico che alcuni controller potrebbero aver bisogno di utilizzare più di un metodo di servizio?
Michael
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.