Dovremmo testare tutti i nostri metodi?

54

Quindi 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 una?". L'intera classe era un gestore (o un servizio se si preferisce chiamarlo così) e quasi tutti i metodi erano semplicemente delegando roba a un DAO quindi era simile a:

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

Una specie di piastra di riscaldamento senza logica (o almeno non considero la delega così semplice come logica) ma una piastra di riscaldamento utile nella maggior parte dei casi (separazione dei livelli ecc.). E abbiamo avuto una discussione piuttosto lunga se dovessi o meno provarlo unitamente (penso che valga la pena ricordare che ho fatto un test unitario del DAO). La sua argomentazione principale era che non era TDD (ovviamente) e che qualcuno avrebbe voluto vedere il test per verificare cosa faccia questo metodo (non so come potrebbe essere più ovvio) o che in futuro qualcuno potrebbe voler cambiare il implementazione e aggiungi una nuova (o più simile a "qualsiasi") logica (nel qual caso suppongo che qualcuno dovrebbe semplicemente testare la logica ).

Questo mi ha fatto pensare, però. Dovremmo cercare di ottenere il più alto tasso di copertura del test? O è semplicemente un'arte per l'arte quindi? Semplicemente non vedo alcun motivo dietro a testare cose come:

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

Ovviamente un test per tale metodo (con mock) mi richiederebbe meno di un minuto, ma suppongo che sia ancora tempo sprecato e un millisecondo più lungo per ogni CI.

Esistono ragioni razionali / non "infiammabili" per le quali si dovrebbe testare ogni singola riga di codice (o quante più possibile)?

    
posta Zenzen 19.01.2012 - 21:40
fonte

10 risposte

44

Vado secondo la regola empirica di Kent Beck:

Prova tutto ciò che potrebbe eventualmente rompersi.

Naturalmente, questo è soggettivo in una certa misura. Per me, i getters / setters banali e one-liner come i tuoi sopra di solito non valgono la pena. Ma poi di nuovo, passo la maggior parte del mio tempo a scrivere test unitari per codice legacy, solo sognando un bel progetto TDD greenfield ... Su questi progetti, le regole sono diverse. Con il codice legacy, l'obiettivo principale è quello di coprire il maggior numero di risorse con il minimo sforzo possibile, pertanto i test unitari tendono ad essere di livello superiore e più complessi, più come test di integrazione se si è pedanti sulla terminologia. E quando stai lottando per ottenere una copertura complessiva del codice dallo 0%, o se riesci solo a superarlo del 25%, i test di getter e setter sono 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 linea vale un test dedicato?". E almeno questi test sono banali da scrivere e veloci da eseguire, quindi non è un grosso problema in ogni caso.

    
risposta data 19.01.2012 - 21:49
fonte
12

Esistono pochi tipi di test delle unità:

  • Basato sullo stato. Agisci e poi asserisci contro lo stato dell'oggetto. Per esempio. Faccio un deposito Quindi controllo se il saldo è aumentato.
  • Valore di ritorno basato. Agisci e asserisci contro il valore di ritorno.
  • Interazione basata. 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 test, allora avrebbe più senso, come ci si aspetterebbe di chiamare un livello di accesso ai dati. Il test fallirebbe inizialmente. Dovresti quindi scrivere il codice di produzione per far passare il test.

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

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

Al momento non c'è alcuna logica, ma non sarà sempre così.

Tuttavia, se si è certi 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 si trova sulla stessa pagina. Non si vuole inviare un messaggio sbagliato al team dicendo "Ehi ragazzi, va bene ignorare il livello del dominio, basta chiamare direttamente il livello di accesso ai dati".

Vorrei anche concentrarmi sul test di altri componenti se ci fosse un test di integrazione per questo metodo. Tuttavia, devo ancora vedere un'azienda con test di integrazione solidi.

Detto questo, non testerei ciecamente tutto. Vorrei stabilire i punti caldi (componenti con elevata complessità e alto rischio di rottura). Vorrei quindi concentrarmi su questi componenti. Non ha senso avere una base di codice in cui il 90% della base di codice è abbastanza semplice ed è coperto da test di unità, quando il restante 10% rappresenta la logica di base del sistema e non sono coperti dai test di unità 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 elevata. La copertura del codice dovrebbe essere un prodotto di una buona serie di test unitari. Ad esempio, puoi scrivere un test che seguirà l'albero e fornirti copertura al 100% di questo metodo, oppure puoi scrivere tre test unitari che ti forniranno anche una copertura del 100%. La differenza è che scrivendo tre test testate i casi limite, invece di solo camminare sull'albero.

    
risposta data 28.03.2014 - 00:43
fonte
8

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

  1. il controllo del tipo sta gestendo parte del problema.
  2. testing gestirà il resto

Per le funzioni standard e semplici, puoi fare affidamento sul controllo dei tipi eseguendo il lavoro e, per il resto, hai bisogno di casi di test.

    
risposta data 19.01.2012 - 22:19
fonte
6

Secondo me la complessità ciclomatica è un parametro. Se un metodo non è abbastanza complesso (come getter e setter). Non è necessario alcun test unitario. Il livello di complessità ciclomatica di McCabe dovrebbe essere maggiore di 1. Un'altra parola dovrebbe contenere almeno un blocco di istruzioni.

    
risposta data 20.10.2012 - 22:03
fonte
1

Di fronte a una domanda filosofica, torna alle esigenze di guida.

Il tuo obiettivo è produrre un software ragionevolmente affidabile a un costo competitivo?

O è in grado di produrre software con la massima affidabilità possibile a prescindere dai costi?

Fino ad un punto, i due obiettivi di qualità e velocità di sviluppo / allineamento dei costi: si impiegano meno tempo a scrivere test che a correggere i difetti.

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

Questo costo può variare (e, se vuoi essere malvagio, così sarà la loro capacità di farti valere quei costi, sia attraverso il mercato o una causa). Non vuoi essere cattivo, quindi contrai completamente quei costi; a volte alcuni test ancora globalmente rendono il mondo più povero dalla loro esistenza.

In breve, se cerchi di applicare ciecamente gli stessi standard a un sito web interno come un software di volo aereo di linea per passeggeri, finirai per andare fuori mercato o finire in prigione.

    
risposta data 28.03.2014 - 00:11
fonte
1

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

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

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

Il DDD in TDD

Il TDD viene spesso citato come avente i principali vantaggi.

  • di difesa
    • Assicurandosi che il codice possa cambiare ma non il suo comportamento .
    • Ciò consente la pratica così importante di refactoring .
    • Ottieni questo TDD o no.
  • design
    • Tu specifica cosa dovrebbe fare qualcosa, come dovrebbe comportarsi prima di implementarlo .
    • Questo spesso significa decisioni di implementazione più informate .
  • Documentazione
    • La suite di test deve essere utilizzata come documentazione specifica (requisiti)
    • L'utilizzo di test per tale scopo significa che la documentazione e l'implementazione sono sempre in uno stato coerente: una modifica a una significa una modifica ad un'altra. Confronta con i requisiti di mantenimento e la progettazione su un documento separato di parole.

Separa la responsabilità dall'implementazione

Come programmatori, è terribilmente allettante pensare che gli attributi siano qualcosa di significativo e getter e setter come una sorta di sovraccarico.

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

È molto più importante sillabare che un oggetto dovrebbe:

Allow its clients to change its state

e

Allow its clients to query its state

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

Un test come

(The Painter class) should store the provided colour

è importante per la documentazione parte di TDD.

Il fatto che l'eventuale implementazione sia banale (attributo) e non porti benefici defense dovrebbe essere sconosciuta a te quando scrivi il test.

La mancanza di ingegneria di andata e ritorno ...

Uno dei problemi chiave nel mondo dello sviluppo del sistema è la mancanza di round-trip engineering 1 - il processo di sviluppo di un sistema è frammentato in sottoprocessi disgiunti gli artefatti di cui (documentazione, codice) sono spesso incoerenti.

1 Brodie, Michael L. "John Mylopoulos: semi di cucito della modellazione concettuale." Modellazione concettuale: fondamenti e applicazioni. Springer Berlin Heidelberg, 2009. 1-9.

... e come TDD lo risolve

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

Progetta prima, implementa più tardi

All'interno di TDD scriviamo per primi il test di accettazione fallito, solo poi scriviamo il codice che consente loro di passare.

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

Perché dovresti escludere setter e getter?

In teoria, è perfettamente possibile all'interno di TDD che una persona scriva il test e un'altra per implementare il codice che la rende passata.

Quindi chiediti:

Should the person writing the tests for a class mention getters and setter.

Poiché getter e setter sono un'interfaccia pubblica per una classe, la risposta è ovviamente , oppure 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

Ci sono alcune ovvie eccezioni a questa regola - funzioni che sono dettagli di implementazione chiari e chiaramente non fanno parte del design del sistema.

Ad esempio, un 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 un'interfaccia public che richiede l'ortografia nella progettazione del componente di sistema.

    
risposta data 18.11.2015 - 15:18
fonte
0

La tua risposta su questo dipende dalla tua filosofia (credo che sia Chicago vs Londra? Sono sicuro che qualcuno lo verificherà). La giuria è ancora fuori su questo approccio più efficace dal punto di vista temporale (perché, dopo tutto, questo è il più grande driver di questo tempo in meno 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. Sono state combattute molte guerre sante. 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.

    
risposta data 21.01.2012 - 02:26
fonte
0

È una domanda complicata.

In senso stretto direi che non è necessario. È meglio scrivere unità di stile BDD e test 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 mettere in discussione il motivo per cui esiste in primo luogo e se è necessario, o se ci sono requisiti nascosti nel codice che non si riflettono nella tua documentazione o storie utente che dovrebbero essere codificate in un caso di test stile BDD.

Personalmente mi piace mantenere la copertura per linea intorno all'85-95% e il check-in della porta principale per assicurare che la copertura del test unitario esistente per linea raggiunga questo livello per tutti i file di codice e che nessun file sia scoperto.

Supponendo che vengano seguite le migliori pratiche di test, questo offre un'abbondanza di copertura senza costringere gli sviluppatori a perdere tempo a cercare di capire come ottenere una copertura aggiuntiva su codice difficile da esercitare o codice banale semplicemente per ragioni di copertura.

    
risposta data 20.01.2012 - 20:50
fonte
-1

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

La sua chiave di lettura dei termini di caratteristiche / comportamenti anziché pensare in termini di metodi e classi. Ovviamente un metodo è qui per fornire supporto per una o più funzionalità, alla fine tutto il tuo codice è stato testato, almeno tutto il codice è importante nella tua base di codice.

Nel tuo scenario, probabilmente questa classe "manager" è ridondante o non necessaria (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 meritare un test unitario perché questa classe non ha alcuna logica aziendale pertinente. Probabilmente hai bisogno di questa classe per far funzionare alcune funzionalità, il test per questa funzione copre questa classe, in questo modo puoi refactoring di questa classe e fare test per verificare che la cosa che conta, le tue funzioni, funzioni ancora dopo il refactoring.

Pensa a caratteristiche / comportamenti non nelle classi di metodo, non posso ripeterlo abbastanza volte.

    
risposta data 28.03.2014 - 03:18
fonte
-4

This made me think, though. Should we strive for the highest test coverage %?

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

getters and setters (unless they actually have some logic in them)

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

"boilerplate" code

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

Are there any rational/not "flammable" reasons to why one should test every single (or as many as he can) line of code?

Non facendolo, potresti perdere alcuni bug molto ovvi. I test unitari sono come una rete sicura per catturare certi tipi di bug e dovresti usarli il più possibile.

E l'ultima cosa: sono su un progetto in cui le persone non vogliono sprecare il loro tempo scrivendo test unitari per qualche "codice semplice", ma in seguito hanno deciso di non scrivere affatto. Alla fine, parti del codice trasformate in grande palla di fango .

    
risposta data 19.01.2012 - 22:22
fonte

Leggi altre domande sui tag