TDD Red-Green-Refactor e se / come testare metodi che diventano privati

91

Per quanto ho capito, molte persone sembrano concordare sul fatto che i metodi privati non dovrebbero essere testati direttamente, ma piuttosto attraverso qualunque metodo pubblico li chiami. Posso vedere il loro punto, ma ho qualche problema con questo quando cerco di seguire le "Tre leggi del TDD", e utilizzare il ciclo "Red - green - refactor". Penso che sia meglio spiegato da un esempio:

Al momento, ho bisogno di un programma in grado di leggere un file (contenente dati separati da tabulazioni) e filtrare tutte le colonne che contengono dati non numerici. Immagino che probabilmente ci siano già alcuni semplici strumenti per farlo, ma ho deciso di implementarlo da zero, soprattutto perché ho pensato che potesse essere un progetto carino e pulito per me fare pratica con TDD.

Quindi, per prima cosa, "metto il cappello rosso", cioè ho bisogno di un test che fallisca. Ho pensato, avrò bisogno di un metodo che trovi tutti i campi non numerici in una riga. Quindi scrivo un semplice test, ovviamente non riesce a compilare immediatamente, quindi inizio a scrivere la funzione stessa, e dopo un paio di cicli avanti e indietro (rosso / verde) ho una funzione funzionante e un test completo.

Successivamente, continuo con una funzione, "gatherNonNumericColumns" che legge il file, una riga alla volta, e chiama la funzione "findNonNumNumericFields" su ogni riga per raccogliere tutte le colonne che alla fine devono essere rimosse. Un paio di cicli rosso-verdi, e ho finito, avendo di nuovo una funzione funzionante e un test completo.

Ora, immagino che dovrei refactoring. Poiché il mio metodo "findNonNumericFields" è stato progettato solo perché ho pensato che mi sarebbe servito quando si implementava "gatherNonNumericColumns", mi sembra ragionevole lasciare che "findNonNumericFields" diventi privato. Tuttavia, ciò interromperà i miei primi test, dal momento che non avrebbero più accesso al metodo che stavano testando.

Quindi, finisco con un metodo privato e una suite di test che lo testano. Dal momento che così tante persone consigliano che i metodi privati non dovrebbero essere testati, sembra che mi sia dipinta in un angolo qui. Ma dove esattamente ho fallito?

Capisco che avrei potuto iniziare a un livello più alto, scrivendo un test che testerà quello che diventerà il mio metodo pubblico (cioè findAndFilterOutAllNonNumericalColumns), ma che sembra in qualche modo contrario al punto di TDD (almeno secondo Uncle Bob): che dovresti passare continuamente tra test di scrittura e codice di produzione e che in qualsiasi momento, tutti i tuoi test hanno funzionato nell'ultimo minuto o giù di lì. Perché se inizio scrivendo un test per un metodo pubblico, ci saranno diversi minuti (o ore, o persino giorni in casi molto complessi) prima di ottenere tutti i dettagli nei metodi privati per lavorare in modo che il test test il pubblico metodo passa.

Quindi, cosa fare? Il TDD (con il ciclo rapido rosso-verde-refactor) non è semplicemente compatibile con i metodi privati? O c'è un difetto nel mio design?

    
posta Henrik Berg 15.04.2015 - 10:51
fonte

15 risposte

42

Unità

Penso di poter individuare esattamente dove il problema è iniziato:

I figured, I'll need a method that finds all the non-numerical fields in a line.

Questo dovrebbe essere seguito immediatamente con la domanda "Sarà un'unità testabile separata a gatherNonNumericColumns o parte dello stesso?"

Se la risposta è " yes, separate ", allora la tua linea di condotta è semplice: quel metodo deve essere pubblico su una classe appropriata, quindi può essere testato come un'unità. La tua mentalità è qualcosa come "Devo testare un metodo e devo anche provare un altro metodo"

Da quello che dici, hai capito che la risposta è " no, fa parte dello stesso ". A questo punto, il tuo piano non dovrebbe più essere completamente scritto e testato findNonNumericFields quindi scrivi gatherNonNumericColumns . Invece, dovrebbe essere semplicemente scrivere gatherNonNumericColumns . Per ora, findNonNumericFields dovrebbe essere solo una parte probabile della destinazione che hai in mente quando scegli il tuo prossimo caso di test rosso e fai il refactoring. Questa volta la tua mentalità è "Devo testare un metodo, e mentre lo faccio dovrei tenere a mente che la mia implementazione finale probabilmente includerà questo altro metodo".

Mantenimento di un ciclo breve

Eseguendo quanto sopra dovrebbe non portare ai problemi che descrivi nel penultimo paragrafo:

Because if I start out by writing a test for a public method, there will be several minutes (or hours, or even days in very complex cases) before I get all the details in the private methods to work so that the test testing the public method passes.

In nessun caso questa tecnica richiede che tu scriva un test rosso che diventerà verde solo quando implementi tutto il findNonNumericFields da zero. Molto più probabilmente, findNonNumericFields inizierà come codice in linea nel metodo pubblico che stai testando, che verrà creato nel corso di diversi cicli e infine estratto durante un refactoring.

Tabella di marcia

Per dare una tabella di marcia approssimativa per questo particolare esempio, non conosco i casi di test esatti che hai usato, ma dici che stavi scrivendo gatherNonNumericColumns come metodo pubblico. Quindi molto probabilmente i casi di test saranno uguali a quelli che hai scritto per findNonNumericFields , ognuno dei quali usa una tabella con una sola riga. Quando lo scenario di una riga è stato completamente implementato e volevi scrivere un test per costringerti ad estrarre il metodo, dovresti scrivere un caso a due righe che richiede di aggiungere l'iterazione.

    
risposta data 15.04.2015 - 17:01
fonte
66

Molte persone pensano che il test unitario sia basato sui metodi; non è. Dovrebbe essere basato sull'unità più piccola che abbia senso. Per la maggior parte delle cose questo significa che la classe è ciò che dovresti testare come entità intera. Non i singoli metodi su di esso.

Ora ovviamente chiamerai i metodi sulla classe, ma dovresti pensare ai test che si applicano all'oggetto black box che hai, quindi dovresti essere in grado di vedere qualsiasi operazione logica fornita dalla classe; queste sono le cose che devi testare. Se la tua classe è così grande che l'operazione logica è troppo complessa, allora hai un problema di progettazione che dovrebbe essere risolto per primo.

Una classe con mille metodi può sembrare testabile, ma se testi singolarmente ciascun metodo non stai veramente testando la classe. Alcune classi potrebbero richiedere di trovarsi in un determinato stato prima che venga chiamato un metodo, ad esempio una classe di rete che richiede una connessione prima di inviare i dati. Il metodo di invio dei dati non può essere considerato indipendentemente dall'intera classe.

Quindi dovresti vedere che i metodi privati sono irrilevanti per il test. Se non puoi esercitare i tuoi metodi privati chiamando l'interfaccia pubblica della tua classe, allora quei metodi privati sono inutili e non verranno comunque utilizzati.

Penso che molte persone provino a trasformare i metodi privati in unità testabili perché sembra facile eseguire test per loro, ma ciò porta a una granularità del test troppo ampia. Martin Fowler dice

Although I start with the notion of the unit being a class, I often take a bunch of closely related classes and treat them as a single unit

che ha molto senso per un sistema orientato agli oggetti, gli oggetti progettati per essere unità. Se vuoi testare i singoli metodi, forse dovresti creare un sistema procedurale come C, o una classe composta interamente da funzioni statiche.

    
risposta data 15.04.2015 - 11:24
fonte
51

Il fatto che i tuoi metodi di raccolta dei dati siano abbastanza complessi da meritare che i test e siano sufficientemente separati dal tuo obiettivo principale per essere metodi propri piuttosto che parte di alcuni punti di loop alla soluzione: rendi questi metodi non privati, ma membri di qualche classe altra che fornisce funzionalità di raccolta / filtro / tabulazione.

Quindi scrivi test per gli stupidi aspetti di munging dei dati della classe helper (ad es. "distinguere i numeri dai personaggi") in un unico posto e verifica il tuo obiettivo principale (ad esempio "ottenere i dati di vendita") in un altro posto, e non è necessario ripetere i test di filtraggio di base nei test per la normale logica aziendale.

In generale, se la tua classe che fa una cosa contiene un codice esteso per fare un'altra cosa richiesta, ma separata dal suo scopo principale, quel codice dovrebbe vivere in un'altra classe e essere chiamato tramite metodi pubblici. Non dovrebbe essere nascosto negli angoli riservati di una classe che solo accidentalmente contiene quel codice. Ciò migliora la testabilità e la comprensibilità allo stesso tempo.

    
risposta data 15.04.2015 - 10:58
fonte
29

Personalmente, sento che sei andato molto oltre nella mentalità implementativa quando hai scritto i test. hai supposto avresti bisogno di determinati metodi. Ma hai davvero bisogno che facciano ciò che la classe dovrebbe fare? La classe fallirebbe se qualcuno arrivasse e li rifattesse internamente? Se tu fossi usando la classe (e questa dovrebbe essere la mentalità del tester a mio parere), potresti davvero preoccuparti di meno se esiste un metodo esplicito per verificare i numeri.

Dovresti testare l'interfaccia pubblica di una classe. L'implementazione privata è privata per un motivo. Non fa parte dell'interfaccia pubblica perché non è necessaria e può cambiare. È un dettaglio di implementazione.

Se scrivi test contro l'interfaccia pubblica, non avrai mai effettivamente il problema che hai incontrato. O puoi creare casi di test per l'interfaccia pubblica che copre i tuoi metodi privati (grandi) o non puoi. In tal caso, potrebbe essere il momento di riflettere seriamente sui metodi privati e magari eliminarli tutti insieme se non possono essere raggiunti comunque.

    
risposta data 15.04.2015 - 11:28
fonte
11

Non si esegue TDD in base a ciò che si prevede che la classe esegua internamente.

I test case dovrebbero basarsi su ciò che la classe / funzionalità / programma deve fare al mondo esterno. Nel tuo esempio, l'utente chiamerà mai la classe del tuo lettore con find all the non-numerical fields in a line?

Se la risposta è "no", allora è un cattivo test da scrivere in primo luogo. Vuoi scrivere il test sul livello di funzionalità a livello di classe / interfaccia - non sul "cosa dovrà implementare il metodo di classe per ottenere questo lavoro", che è ciò che è il tuo test. / p>

Il flusso di TDD è:

  • rosso (quale classe / oggetto / funzione / etc sta facendo al mondo esterno)
  • verde (scrivi il codice minimo per far funzionare questa funzione del mondo esterno)
  • refactor (qual è il codice migliore per farlo funzionare)

NON è da fare "perché avrò bisogno di X in futuro come metodo privato, lasciatemi implementare per prima cosa. "Se ti accorgi di farlo, stai facendo la fase" rossa "in modo errato. Sembra che questo sia il tuo problema qui.

Se ti ritrovi spesso a scrivere test per metodi che diventano metodi privati, stai facendo una delle poche cose:

  • Non comprendi correttamente l'interfaccia / casi d'uso a livello pubblico abbastanza bene da scrivere un test per loro
  • Cambiare radicalmente design e refactoring di diversi test (che possono essere una buona cosa, a seconda che tale funzionalità venga testata con test più recenti)
risposta data 16.04.2015 - 05:08
fonte
9

Stai incontrando un malinteso comune con i test in generale.

La maggior parte delle persone che sono nuove ai test inizia a pensare in questo modo:

  • scrivi un test per la funzione F
  • implementa F
  • scrivi un test per la funzione G
  • implementa G utilizzando una chiamata a F
  • scrivi un test per una funzione H
  • implementa H utilizzando una chiamata a G

e così via.

Il problema qui è che in effetti non hai un test unitario per la funzione H. Il test che dovrebbe testare H sta effettivamente testando H, G e F contemporaneamente.

Per risolvere questo devi capire che le unità testabili non devono mai dipendere l'una dall'altra, ma piuttosto dalle loro interfacce . Nel tuo caso, dove le unità sono semplici funzioni, le interfacce sono solo la loro firma di chiamata. È quindi necessario implementare G in modo che possa essere utilizzato con la funzione qualsiasi con la stessa firma di F.

Come esattamente ciò può essere fatto dipende dal tuo linguaggio di programmazione. In molte lingue è possibile passare funzioni (o puntatori ad esse) come argomenti ad altre funzioni. Ciò ti consentirà di testare ciascuna funzione separatamente.

    
risposta data 15.04.2015 - 17:53
fonte
8

I test che scrivi durante lo sviluppo di Test Driven dovrebbero assicurarsi che una classe implementa correttamente la sua API pubblica, assicurando al tempo stesso che tale API pubblica sia facile da testare e utilizzare.

È possibile utilizzare tutti i metodi privati per implementare tale API, ma non è necessario creare test tramite TDD - la funzionalità verrà testata perché l'API pubblica funzionerà correttamente.

Ora supponiamo che i tuoi metodi privati siano abbastanza complicati da meritare test autonomi, ma non hanno senso come parte dell'API pubblica della tua classe originale. Beh, questo probabilmente significa che dovrebbero essere in realtà metodi pubblici su un'altra classe, una delle quali la tua classe originale sfrutta la propria implementazione.

Testando solo l'API pubblica, è molto più semplice modificare i dettagli di implementazione in futuro. I test non utili ti infastidiranno solo in un secondo momento, quando dovranno essere riscritti per supportare alcuni eleganti refactoring che hai appena scoperto.

    
risposta data 15.04.2015 - 19:49
fonte
4

Penso che la risposta giusta sia la conclusione a cui sei arrivato riguardo ai metodi pubblici. Inizierai scrivendo un test che chiama quel metodo. Fallirebbe quindi tu crei un metodo con quel nome che non fa nulla. Allora forse hai ragione un test che controlla un valore di ritorno.

(Non sono del tutto chiaro su cosa funzioni la tua funzione. Restituisce una stringa con il contenuto del file con i valori non numerici eliminati?)

Se il tuo metodo restituisce una stringa, verifichi il valore restituito. Quindi continui a costruirlo.

Penso che tutto ciò che accade in un metodo privato dovrebbe essere nel metodo pubblico a un certo punto durante il processo, e poi spostato solo nel metodo privato come parte di un passo di refactoring. Il refactoring non richiede di avere test falliti, per quanto ne so. Hai solo bisogno di test falliti quando aggiungi funzionalità. Devi solo eseguire i test dopo il refactoring per assicurarti che passino tutti.

    
risposta data 16.04.2015 - 00:04
fonte
3

it feels like I've painted myself into a corner here. But where exactly did I fail?

C'è un vecchio adagio.

When you fail to plan, you plan to fail.

La gente sembra pensare che quando fai il TDD, ti siedi, scrivi dei test e il design si realizzerà magicamente. Questo non è vero. Devi avere un piano di alto livello in corso. Ho trovato che ottengo i miei migliori risultati da TDD quando progetto l'interfaccia (API pubblica) prima. Personalmente, creo un interface effettivo che definisce prima la classe.

gasp Ho scritto un po 'di "codice" prima di scrivere qualsiasi test! Beh no. Non l'ho fatto Ho scritto un contratto da seguire, un design . Sospetto che potresti ottenere risultati simili annotando un diagramma UML su carta millimetrata. Il punto è che devi avere un piano. TDD non è una licenza per fare hacking a un pezzo di codice.

Mi sento davvero come "Test First" è un termine improprio. Design prima quindi test.

Naturalmente, ti preghiamo di seguire i consigli che altri hanno dato sull'estrarre più classi dal tuo codice. Se sentite strongmente la necessità di testare gli interni di una classe, estraete quelli interni in un'unità facilmente testata e iniettatela.

    
risposta data 17.04.2015 - 03:37
fonte
2

Ricorda che anche i test possono essere sottoposti a refactoring! Se rendi un metodo privato, stai riducendo l'API pubblica e quindi è perfettamente accettabile eliminare alcuni test corrispondenti per quella "funzionalità persa" (AKA ha ridotto la complessità).

Altri hanno affermato che il tuo metodo privato verrà chiamato come parte degli altri tuoi test API, altrimenti sarà irraggiungibile e quindi eliminabile. In effetti, le cose sono più dettagliate se pensiamo a percorsi di esecuzione .

Ad esempio, se abbiamo un metodo pubblico che esegue la divisione, potremmo voler testare il percorso che si traduce in una divisione per zero. Se rendiamo il metodo privato, otteniamo una scelta: o possiamo considerare il percorso della divisione per zero, o possiamo eliminare quel percorso considerando come viene chiamato dagli altri metodi.

In questo modo, possiamo buttare via alcuni test (ad es. dividere per zero) e rifattorizzare gli altri in termini di API pubblica rimanente. Naturalmente, in un mondo ideale i test esistenti si occupano di tutti quei percorsi rimanenti, ma la realtà è sempre un compromesso;)

    
risposta data 15.04.2015 - 16:33
fonte
2

Ci sono momenti in cui un metodo privato può essere reso un metodo pubblico di un'altra classe.

Ad esempio, potresti avere metodi privati che non sono thread-safe e lasciare la classe in uno stato temporaneo. Questi metodi possono essere spostati in una classe separata che è tenuta privatamente dalla tua prima classe. Quindi, se la tua classe è una coda, potresti avere una classe InternalQueue con metodi pubblici e la classe Queue mantenere l'istanza InternalQueue in privato. Ciò consente di testare la coda interna e inoltre chiarisce cosa sono le singole operazioni su InternalQueue.

(Questo è più ovvio quando immaginate che non ci fosse una classe List e se provaste ad implementare le funzioni List come metodi privati nella classe che li usa).

    
risposta data 15.04.2015 - 20:49
fonte
0

Mi chiedo perché la tua lingua abbia solo due livelli di privacy, completamente pubblico e completamente privato.

Puoi organizzare i tuoi metodi non pubblici come pacchetti accessibili o qualcosa del genere? Quindi metti i tuoi test nello stesso pacchetto e divertiti a testare i meccanismi interni che non fanno parte dell'interfaccia pubblica. Il tuo sistema di costruzione escluderà i test quando costruisci un file binario di rilascio.

Ovviamente a volte è necessario disporre di metodi veramente privati, non accessibili a nient'altro che alla classe che definisce. Spero che tutti questi metodi siano molto piccoli. In generale, mantenere i metodi piccoli (ad esempio sotto le 20 righe) aiuta molto: test, manutenzione e la semplice comprensione del codice diventano più semplici.

    
risposta data 15.04.2015 - 17:01
fonte
0

Di tanto in tanto ho urtato metodi privati per proteggerli per consentire test a grana più fine (più severi rispetto alle API pubbliche esposte). Questa dovrebbe essere l'eccezione (si spera molto rara) piuttosto che la regola, ma può essere utile in alcuni casi specifici che potresti incontrare. Inoltre, è qualcosa che non vorresti considerare affatto quando crei un'API pubblica, più di "cheat" che puoi usare su un software di uso interno in quelle rare situazioni.

    
risposta data 15.04.2015 - 18:49
fonte
0

Ho provato questo e ho sentito il tuo dolore.

La mia soluzione era:

interrompi il test come costruire un monolite.

Ricorda che quando hai scritto una serie di test, diciamo 5, per eliminare alcune funzionalità, non devi tenere tutti questi test intorno , specialmente quando questo diventa parte di qualcos'altro.

Ad esempio, ho spesso:

  • test di livello basso 1
  • codice per incontrarlo
  • test di basso livello 2
  • codice per incontrarlo
  • test di basso livello 3
  • codice per incontrarlo
  • test di basso livello 4
  • codice per incontrarlo
  • test di basso livello 5
  • codice per incontrarlo

quindi ho

  • test di livello basso 1
  • test di basso livello 2
  • test di basso livello 3
  • test di basso livello 4
  • test di basso livello 5

Tuttavia, se ora aggiungo funzioni di livello superiore che lo chiamano, che hanno molti test, I potrebbe essere in grado di ridurre ora quei test di basso livello semplicemente:

  • test di livello basso 1
  • test di basso livello 5

Il diavolo è nei dettagli e la capacità di farlo dipenderà dalle circostanze.

    
risposta data 19.04.2015 - 13:27
fonte
-2

Il sole gira intorno alla terra o alla terra intorno al sole? Secondo Einstein la risposta è sì, o entrambi poiché entrambi i modelli differiscono solo dal punto di vista, allo stesso modo l'incapsulamento e lo sviluppo guidato dai test sono solo in conflitto perché pensiamo che lo siano. Ci sediamo qui come Galileo e il papa, lanciandosi insulti l'un l'altro: pazzo, non vedi che anche i metodi privati hanno bisogno di essere testati? eretico, non spezzare l'incapsulamento! Allo stesso modo, quando riconosciamo che la verità è più grande di quanto pensassimo, possiamo provare qualcosa come incapsulare i test per le interfacce private in modo che i test per le interfacce pubbliche non interrompano l'incapsulamento.

Prova questo: aggiungi due metodi, uno che non ha input ma justs restituisce il numero di test privati e uno che prende un numero di test come parametro e restituisce pass / fail.

    
risposta data 18.04.2015 - 16:52
fonte

Leggi altre domande sui tag