Dovremmo testare tutti i nostri metodi?


62

Così oggi ho avuto un colloquio con il mio compagno di squadra sui test unitari. Tutto è iniziato quando mi ha chiesto "hey, dove sono i test per quella classe, ne vedo solo uno?". Tutta la classe era un manager (o un servizio se preferite chiamarlo così) e quasi tutti i metodi stavano semplicemente delegando roba a un DAO, quindi era simile a:

SomeClass getSomething(parameters) {
    return myDao.findSomethingBySomething(parameters);
}

Una specie di boilerplate senza logica (o almeno non considero una delega semplice come logica) ma nella maggior parte dei casi un utile boilerplate (separazione degli strati ecc.). E abbiamo avuto una discussione piuttosto lunga sull'opportunità o meno di testare l'unità (penso che valga la pena ricordare che ho testato completamente l'unità DAO). Il suo argomento principale è che non era TDD (ovviamente) e che qualcuno potrebbe voler vedere il test per verificare cosa fa questo metodo (non so come potrebbe essere più ovvio) o che in futuro qualcuno potrebbe voler cambiare il implementazione e aggiungere una nuova (o più come "qualsiasi") logica (nel qual caso suppongo che qualcuno dovrebbe semplicemente testare quella logica ).

Questo mi ha fatto pensare, però. Dobbiamo impegnarci per ottenere la percentuale di test più elevata%? O è semplicemente un'arte per l'arte? Semplicemente non vedo alcun motivo dietro test di cose come:

  • Getter e setter (a meno che non abbiano effettivamente qualche logica in essi)
  • codice "boilerplate"

Ovviamente un test per un tale metodo (con beffe) mi richiederebbe meno di un minuto, ma immagino che sia ancora tempo perso e un millisecondo in più per ogni CI.

Ci sono ragioni razionali / non "infiammabili" per cui si dovrebbe testare ogni singola (o quante più è possibile) riga di codice?


2
Sto ancora decidendo su questa domanda, ma ecco un discorso di qualcuno che ha deciso che la risposta è "no". Ian Cooper: TDD, dove è andato tutto storto Per riassumere questo grande discorso, dovresti testare al di fuori e testare nuovi comportamenti e non nuovi metodi.
Daniel Kaplan,

Questa è davvero una bella chiacchierata, assolutamente da vedere, un discorso che apre gli occhi a molte persone, lo adoro. Ma penso che la risposta non sia "no". È "sì, ma indirettamente". Ian Cooper parla dell'architettura esagonale e testa caratteristiche / comportamenti che deridono / rubano le porte. In questo caso queste porte sono DAO e questo "gestore / servizio" viene testato non con un test unitario solo per questa classe ma con un "test unit" (unità nella definizione di Ian Cooper che sono completamente d'accordo) che testano alcune funzionalità nel tuo dominio che utilizzano questo gestore / servizio.
AlfredoCasado,


Dipenderà dal tuo sistema in una certa misura, se stai sviluppando un sistema con un livello di certificazione di sicurezza da moderato a elevato dovrai coprire tutti i metodi indipendentemente dalla banalità
jk.

Risposte:


49

Seguo la regola empirica di Kent Beck:

Metti alla prova tutto ciò che potrebbe rompersi.

Certo, questo è soggettivo in una certa misura. Per me, banali getter / setter e one-liner come i tuoi sopra di solito non ne valgono la pena. Ma ancora una volta, passo la maggior parte del mio tempo a scrivere unit test per il codice legacy, solo sognando un bel progetto TDD greenfield ... Su tali progetti, le regole sono diverse. Con il codice legacy, l'obiettivo principale è quello di coprire la maggior parte del terreno con il minor sforzo possibile, quindi i test unitari tendono ad essere di livello superiore e più complessi, più simili ai test di integrazione se si è pedanti sulla terminologia. E quando stai lottando per ottenere una copertura totale del codice dallo 0%, o sei semplicemente riuscito a superare il 25%, l'unità di test getter e setter è l'ultima delle tue preoccupazioni.

OTOH in un progetto TDD greenfield, potrebbe essere più pratico scrivere test anche per tali metodi. Soprattutto perché hai già scritto il test prima di avere la possibilità di iniziare a chiedermi "questa riga merita un test dedicato?". E almeno questi test sono banali da scrivere e veloci da eseguire, quindi non è un grosso problema in entrambi i casi.


Ah, ho completamente dimenticato quella citazione! Immagino che lo userò come argomento principale perché francamente - cosa può rompere qui? Non molto. L'unica cosa che può rompersi è l'invocazione del metodo e se ciò accade significa che è successo qualcosa di veramente brutto. Grazie!
Zenzen,

5
@Zenzen: "cosa può rompersi qui? Non molto." - Quindi può rompersi. Solo un piccolo errore di battitura. O qualcuno aggiunge del codice. O rovina la dipendenza. Penso davvero che Beck affermerebbe che il tuo esempio principale si qualifica come fragile. Getter e setter, meno, anche se mi sono sorpreso in un errore di copia / incolla, anche allora. La vera domanda è, se è troppo banale scrivere un test, perché esiste?
pdr

1
La quantità di tempo che hai trascorso a pensarci già avrebbe potuto scrivere il test. dico scrivere il test, non lasciare quando non scrivere un test come area grigia, appariranno più finestre rotte.
kett_chup,

1
Aggiungerò che la mia esperienza generale è che testare getter e setter è in qualche modo prezioso a lungo termine, ma a bassa priorità. Il motivo è perché perché ha "zero" possibilità di trovare un bug ora, non puoi garantire che un altro sviluppatore non aggiungerà qualcosa in tre mesi ("solo una semplice istruzione if") che avrà la possibilità di rompersi . Avere un test unitario sul posto protegge da questo. Allo stesso tempo, non è davvero una priorità eccessiva, perché non troverai nulla in questo modo presto.
dclements

7
Testare alla cieca tutto ciò che potrebbe rompersi non ha senso. Ci deve essere una strategia in cui i componenti ad alto rischio vengono testati per primi.
CodeART

13

Esistono alcuni tipi di test unitari:

  • Basato sullo stato. Agisci e poi asserisci contro lo stato dell'oggetto. Ad esempio, faccio un deposito. Quindi controllo per vedere se il saldo è aumentato.
  • Valore di ritorno basato. Agisci e asserisci contro il valore di ritorno.
  • Basato sull'interazione. Verifica che il tuo oggetto abbia chiamato un altro oggetto. Questo sembra essere quello che stai facendo nel tuo esempio.

Se dovessi scrivere prima il tuo test, allora avrebbe più senso - come ti aspetteresti di chiamare un livello di accesso ai dati. Il test inizialmente fallirebbe. Quindi scrivere il codice di produzione per passare il test.

Idealmente dovresti testare il codice logico, ma le interazioni (oggetti che chiamano altri oggetti) sono ugualmente importanti. Nel tuo caso, lo farei

  • Verifica di aver chiamato il livello di accesso ai dati con il parametro esatto che è stato passato.
  • Controlla che sia stato chiamato solo una volta.
  • Verifica che restituisca esattamente ciò che mi è stato dato dal livello di accesso ai dati. Altrimenti potrei anche restituire null.

Attualmente non c'è logica lì, ma non sarà sempre così.

Tuttavia, se sei sicuro che non ci sarà logica in questo metodo ed è probabile che rimanga lo stesso, allora prenderei in considerazione la possibilità di chiamare il livello di accesso ai dati direttamente dal consumatore. Lo farei solo se il resto della squadra fosse nella stessa pagina. Non vuoi inviare un messaggio sbagliato al team dicendo "Ehi ragazzi, va bene ignorare il livello di dominio, basta chiamare direttamente il livello di accesso ai dati".

Mi concentrerei anche sul test di altri componenti se ci fosse un test di integrazione per questo metodo. Devo ancora vedere un'azienda con solidi test di integrazione.

Detto questo, non testerei ciecamente tutto. Vorrei stabilire i punti caldi (componenti con elevata complessità e alto rischio di rottura). Mi concentrerei quindi su questi componenti. Non ha senso disporre di una base di codice in cui il 90% di base di codice è piuttosto semplice ed è coperto da test unitari, mentre il restante 10% rappresenta la logica di base del sistema e non sono coperti da test unitari a causa della loro complessità.

Infine, qual è il vantaggio di testare questo metodo? Quali sono le implicazioni se questo non funziona? Sono catastrofici? Non cercare di ottenere una copertura di codice elevato. La copertura del codice dovrebbe essere un prodotto secondario di una buona serie di test unitari. Ad esempio, puoi scrivere un test che percorrerà l'albero e ti fornirà il 100% di copertura di questo metodo, oppure puoi scrivere tre test unitari che ti daranno anche il 100% di copertura. La differenza è che scrivendo tre prove si verificano casi limite, invece di camminare semplicemente sull'albero.


Perché dovresti verificare che il tuo DAL sia stato chiamato solo una volta?
Marjan Venema,

9

Ecco un buon modo di pensare alla qualità del tuo software:

  1. il controllo del tipo sta gestendo parte del problema.
  2. i test gestiranno il resto

Per le funzioni boilerplate e banali, puoi fare affidamento sul controllo del tipo facendo il suo lavoro, e per il resto, hai bisogno di casi di test.


Ovviamente il controllo del tipo funziona solo se stai usando tipi specifici nel tuo codice e stai lavorando un linguaggio compilato o ti assicuri altrimenti che un controllo di analisi statica venga eseguito frequentemente, ad esempio come parte di CI.
bdsl,

6

Secondo me la complessità ciclomatica è un parametro. Se un metodo non è abbastanza complesso (come getter e setter). Non sono necessari test unitari. Il livello di complessità ciclomatica di McCabe dovrebbe essere superiore a 1. Un'altra parola dovrebbe contenere almeno 1 istruzione di blocco.


Ricorda che alcuni getter o setter hanno effetti collaterali (sebbene sia scoraggiato e considerato cattiva pratica nella maggior parte dei casi), quindi anche le modifiche al codice sorgente potrebbero influire su di esso.
Andrzej Bobak,

3

Un clamoroso SÌ con TDD (e con alcune eccezioni)

Va bene controverso, ma direi che a chiunque risponda "no" a questa domanda manca un concetto fondamentale di TDD.

Per me, la risposta è un clamoroso se segui TDD. Se non lo sei, allora no è una risposta plausibile.

Il DDD in TDD

TDD è spesso citato come avere i principali vantaggi.

  • Difesa
    • Garantire che il codice possa cambiare ma non il suo comportamento .
    • Ciò consente l'importantissima pratica del refactoring .
    • Ottieni questo TDD o no.
  • Design
    • Si specifica che cosa dovrebbe fare qualcosa, come dovrebbe comporta prima di implementare esso.
    • Questo spesso significa decisioni di implementazione più informate .
  • Documentazione
    • La suite di test dovrebbe fungere da documentazione delle specifiche (requisiti).
    • L'uso di test a tale scopo significa che la documentazione e l'implementazione sono sempre in uno stato coerente: una modifica a una significa una modifica a un'altra. Confrontare con i requisiti di conservazione e progettare su un documento word separato.

Separare la responsabilità dall'attuazione

Come programmatori, è terribilmente allettante pensare agli attributi come a qualcosa di significativo e ai getter e setter come a una sorta di sovraccarico.

Ma gli attributi sono un dettaglio di implementazione, mentre setter e getter sono l'interfaccia contrattuale che fa effettivamente funzionare i programmi.

È molto più importante precisare che un oggetto dovrebbe:

Consenti ai suoi clienti di cambiare il suo stato

e

Consenti ai suoi client di interrogare il suo stato

quindi come viene effettivamente memorizzato questo stato (per il quale un attributo è il modo più comune, ma non l'unico).

Un test come

(The Painter class) should store the provided colour

è importante per la parte relativa alla documentazione di TDD.

Il fatto che l'eventuale implementazione sia banale (attributo) e non porti benefici alla difesa dovrebbe essere ignoto quando si scrive il test.

La mancanza di ingegneria di andata e ritorno ...

Uno dei problemi chiave nel mondo dello sviluppo del sistema è la mancanza di ingegneria di andata e ritorno 1 - il processo di sviluppo di un sistema è frammentato in sottoprocessi sconnessi i cui artefatti (documentazione, codice) sono spesso incoerenti.

1 Brodie, Michael L. "John Mylopoulos: cucire semi di modelli concettuali." Modellazione concettuale: fondamenti e applicazioni. Springer Berlin Heidelberg, 2009. 1-9.

... e come TDD lo risolve

È la parte della documentazione di TDD che garantisce che le specifiche del sistema e il suo codice siano sempre coerenti.

Progettare prima, implementare in seguito

All'interno di TDD scriviamo prima i test di accettazione non riusciti, solo poi scriviamo il codice che li lascia passare.

All'interno del BDD di livello superiore, scriviamo prima gli scenari, quindi li facciamo passare.

Perché dovresti escludere setter e getter?

In teoria, all'interno di TDD è perfettamente possibile per una persona scrivere il test e un'altra per implementare il codice che lo fa passare.

Quindi chiediti:

La persona che scrive i test per una classe deve menzionare getter e setter.

Poiché getter e setter sono un'interfaccia pubblica per una classe, la risposta è ovviamente , o non ci sarà modo di impostare o interrogare lo stato di un oggetto.

Ovviamente, se scrivi prima il codice, la risposta potrebbe non essere così chiara.

eccezioni

Esistono alcune ovvie eccezioni a questa regola: funzioni che sono dettagli di implementazione chiari e chiaramente non fanno parte della progettazione del sistema.

Ad esempio, a il metodo locale 'B ()':

function A() {

    // B() will be called here    

    function B() {
        ...
    }
} 

O la funzione privata square()qui:

class Something {
private:
    square() {...}
public:
    addAndSquare() {...}
    substractAndSquare() {...}
}

O qualsiasi altra funzione che non fa parte di publicun'interfaccia che necessita dell'ortografia nella progettazione del componente di sistema.


1

Di fronte a una domanda filosofica, ricadere sui requisiti di guida.

Il tuo obiettivo è produrre software ragionevolmente affidabile a costi competitivi?

O è per produrre software con la massima affidabilità possibile quasi indipendentemente dal costo?

Fino a un certo punto, i due obiettivi di qualità e velocità / costi di sviluppo si allineano: dedichi meno tempo alla scrittura dei test che alla correzione dei difetti.

Ma oltre quel punto, non lo fanno. Non è così difficile arrivare, diciamo, a un bug segnalato per sviluppatore al mese. Dimezzarlo a uno ogni due mesi rilascia solo un budget di forse un giorno o due, e che molti test extra probabilmente non dimezzeranno il tasso di difetto. Quindi non è più una semplice vittoria / vittoria; devi giustificarlo in base al costo del difetto per il cliente.

Questo costo varierà (e, se vuoi essere malvagio, così anche la loro capacità di farti valere tali costi, sia attraverso il mercato che una causa legale). Non vuoi essere malvagio, quindi conti quei costi per intero; a volte alcuni test ancora a livello globale rendono il mondo più povero per la loro esistenza.

In breve, se si tenta di applicare ciecamente gli stessi standard a un sito Web interno del software di volo per aereo di linea passeggeri, si finisce per finire fuori dal mercato o in prigione.


0

La tua risposta dipende dalla tua filosofia (credi che sia Chicago vs Londra? Sono sicuro che qualcuno lo cercherà). La giuria è ancora fuori su questo approccio più efficace in termini di tempo (perché, dopo tutto, è il più grande driver di questo, meno tempo speso per le correzioni).

Alcuni approcci dicono di testare solo l'interfaccia pubblica, altri dicono di testare l'ordine di ogni chiamata di funzione in ogni funzione. Molte guerre sante sono state combattute. Il mio consiglio è di provare entrambi gli approcci. Scegli un'unità di codice e fallo come X e un'altra come Y. Dopo alcuni mesi di test e integrazione, torna indietro e vedi quale si adatta meglio alle tue esigenze.


0

È una domanda difficile.

A rigor di termini, direi che non è necessario. È meglio scrivere test di unità e stile di sistema BDD a livello di sistema che assicurino che i requisiti aziendali funzionino come previsto in scenari positivi e negativi.

Detto questo, se il tuo metodo non è coperto da questi casi di test, devi prima chiederti perché esiste e se è necessario o se nel codice sono presenti requisiti nascosti che non si riflettono nella documentazione o nelle storie utente che dovrebbe essere codificato in un caso di test in stile BDD.

Personalmente mi piace mantenere la copertura per linea intorno all'85-95% e gate i check-in per continuare a garantire che la copertura dei test unitari esistenti per riga raggiunga questo livello per tutti i file di codice e che nessun file venga scoperto.

Supponendo che vengano seguite le migliori pratiche di test, questo fornisce molta copertura senza costringere gli sviluppatori a perdere tempo cercando di capire come ottenere una copertura aggiuntiva su codice difficile da esercitare o codice banale semplicemente per motivi di copertura.


-1

Il problema è la domanda stessa, non è necessario testare tutti i "methdos" o tutte le "classi" necessarie per testare tutte le funzionalità dei sistemi.

Il suo pensiero chiave in termini di caratteristiche / comportamenti invece di pensare in termini di metodi e classi. Naturalmente un metodo è qui per fornire supporto per una o più funzionalità, alla fine tutto il codice è testato, almeno tutto il codice è importante nella tua base di codice.

Nel tuo scenario, probabilmente questa classe "manager" è ridondante o superflua (come tutte le classi con un nome che contiene la parola "manager"), o forse no, ma sembra un dettaglio di implementazione, probabilmente questa classe non merita un'unità prova perché questa classe non ha alcuna logica aziendale rilevante. Probabilmente hai bisogno di questa classe per far funzionare alcune funzionalità, il test per questa funzionalità copre questa classe, in questo modo puoi refactoring questa classe e avere test che verificano che la cosa che conta, le tue funzionalità, funzioni ancora dopo il refactor.

Pensa in caratteristiche / comportamenti non in classi di metodi, non posso ripeterlo abbastanza volte.


-4

Questo mi ha fatto pensare, però. Dobbiamo impegnarci per ottenere la percentuale di test più elevata%?

Sì, idealmente al 100%, ma alcune cose non sono testabili in unità.

Getter e setter (a meno che non abbiano effettivamente qualche logica in essi)

Getter / setter sono stupidi - semplicemente non li usano. Invece, metti la tua variabile membro nella sezione pubblica.

codice "boilerplate"

Ottieni il codice comune e testalo l'unità. Dovrebbe essere così semplice.

Ci sono ragioni razionali / non "infiammabili" per cui si dovrebbe testare ogni singola (o quante più è possibile) riga di codice?

In caso contrario, potresti perdere alcuni bug molto ovvi. I test unitari sono come una rete sicura per catturare determinati tipi di bug e dovresti usarli il più possibile.

E l'ultima cosa: sono in un progetto in cui le persone non volevano perdere tempo a scrivere unit test per un "codice semplice", ma in seguito hanno deciso di non scrivere affatto. Alla fine, parti del codice si sono trasformate in una grande palla di fango .


Bene, una cosa è chiara: non intendevo dire che non uso i test TDD / write. Piuttosto il contrario. So che i test potrebbero trovare bug a cui non avevo pensato, ma cosa c'è da testare qui? Penso semplicemente che tale metodo sia uno di quelli "non testabili in unità". Come ha detto Péter Török (citando Kent Beck) dovresti testare cose che potrebbero rompersi. Cosa potrebbe rompersi qui? Non molto (c'è solo una semplice delegazione in questo metodo). POSSO scrivere un test unitario ma avrà semplicemente una derisione del DAO e un'affermazione, non molti test. Per quanto riguarda getter / setter, alcuni framework li richiedono.
Zenzen,

1
Inoltre, dal momento che non l'ho notato "Prendi il codice comune e testalo l'unità. Dovrebbe essere così semplice". Che cosa vuoi dire con questo? È una classe di servizio (in un livello di servizio tra la GUI e il DAO), è comune all'intera app. Non riesco davvero a renderlo più generico (poiché accetta alcuni parametri e chiama un determinato metodo nel DAO). L'unica ragione per cui è lì è aderire all'architettura a strati dell'applicazione in modo che la GUI non chiamerà direttamente il DAO.
Zenzen,

20
-1 per "Getter / setter sono stupidi - semplicemente non li usano. Invece, metti la tua variabile membro nella sezione pubblica". - Molto sbagliato. Questo è stato discusso più volte su SO . L'uso di campi pubblici ovunque è in realtà peggiore anche rispetto all'utilizzo di getter e setter ovunque.
Péter Török,
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.