Non mi piace testare la funzionalità privata per un paio di motivi. Sono i seguenti (questi sono i punti principali per le persone TLDR):
- In genere quando sei tentato di testare il metodo privato di una classe, è un odore di design.
- Puoi testarli attraverso l'interfaccia pubblica (che è come li vuoi testare, perché è così che il client li chiamerà / li utilizzerà). Puoi ottenere un falso senso di sicurezza vedendo il via libera su tutti i test di superamento per i tuoi metodi privati. È molto meglio / più sicuro testare casi limite sulle funzioni private attraverso l'interfaccia pubblica.
- Rischi una duplicazione grave dei test (test che sembrano / sembrano molto simili) testando metodi privati. Ciò ha conseguenze importanti quando i requisiti cambiano, poiché molti più test del necessario si interromperanno. Può anche metterti in una posizione in cui è difficile eseguire il refactoring a causa della tua suite di test ... che è la massima ironia, perché la suite di test è lì per aiutarti a riprogettare e refactoring in sicurezza!
Spiegherò ciascuno di questi con un esempio concreto. Si scopre che 2) e 3) sono in qualche modo collegati in modo complesso, quindi il loro esempio è simile, anche se li considero motivi separati per cui non dovresti testare metodi privati.
Ci sono momenti in cui testare metodi privati è appropriato, è importante essere consapevoli dei lati negativi sopra elencati. Lo esaminerò più dettagliatamente in seguito.
Vado anche oltre perché TDD non è una scusa valida per testare metodi privati alla fine.
Rifattorizzare la tua via d'uscita da un cattivo design
Uno dei più comuni (anti) sostenitori che vedo è ciò che Michael Feathers chiama una classe "Iceberg" (se non sai chi è Michael Feathers, vai a comprare / leggere il suo libro "Lavorare efficacemente con il codice legacy". una persona che vale la pena sapere se sei un ingegnere / sviluppatore di software professionale). Ci sono altri (anti) schemi che fanno sorgere questo problema, ma questo è di gran lunga il più comune in cui mi sono imbattuto. Le classi "Iceberg" hanno un metodo pubblico e il resto è privato (motivo per cui è allettante testare i metodi privati). Si chiama classe "Iceberg" perché di solito esiste un metodo pubblico solitario, ma il resto della funzionalità è nascosto sott'acqua sotto forma di metodi privati.
Ad esempio, potresti voler testare GetNextToken()
chiamandolo su una stringa in successione e vedendo che restituisce il risultato atteso. Una funzione come questa merita un test: quel comportamento non è banale, specialmente se le tue regole di tokenizzazione sono complesse. Facciamo finta che non sia così complesso, e vogliamo solo legare in token delimitati dallo spazio. Quindi scrivi un test, forse sembra qualcosa del genere (qualche codice psuedo agnostico in lingua, si spera che l'idea sia chiara):
TEST_THAT(RuleEvaluator, canParseSpaceDelimtedTokens)
{
input_string = "1 2 test bar"
re = RuleEvaluator(input_string);
ASSERT re.GetNextToken() IS "1";
ASSERT re.GetNextToken() IS "2";
ASSERT re.GetNextToken() IS "test";
ASSERT re.GetNextToken() IS "bar";
ASSERT re.HasMoreTokens() IS FALSE;
}
Bene, sembra davvero carino. Vorremmo assicurarci di mantenere questo comportamento mentre apportiamo modifiche. Ma GetNextToken()
è una funzione privata ! Quindi non possiamo provarlo in questo modo, perché non verrà nemmeno compilato (supponendo che stiamo usando un linguaggio che impone effettivamente pubblico / privato, a differenza di alcuni linguaggi di scripting come Python). Ma che dire di cambiare la RuleEvaluator
classe per seguire il principio di responsabilità singola (principio di responsabilità singola)? Ad esempio, sembra che un parser, un tokenizer e un valutatore siano bloccati in una classe. Non sarebbe meglio separare queste responsabilità? Inoltre, se crei una Tokenizer
classe, i suoi metodi pubblici sarebbero HasMoreTokens()
e GetNextTokens()
. La RuleEvaluator
classe potrebbe avere unTokenizer
oggetto come membro. Ora, possiamo mantenere lo stesso test di cui sopra, tranne per il fatto che stiamo testando la Tokenizer
classe anziché laRuleEvaluator
classe.
Ecco come potrebbe apparire in UML:
Nota che questo nuovo design aumenta la modularità, quindi potresti potenzialmente riutilizzare queste classi in altre parti del tuo sistema (prima che non potessi, i metodi privati non sono riutilizzabili per definizione). Questo è il principale vantaggio di abbattere RuleEvaluator, insieme a una maggiore comprensibilità / località.
Il test sembrerebbe estremamente simile, tranne che questa volta verrà effettivamente compilato poiché il GetNextToken()
metodo è ora pubblico sulla Tokenizer
classe:
TEST_THAT(Tokenizer, canParseSpaceDelimtedTokens)
{
input_string = "1 2 test bar"
tokenizer = Tokenizer(input_string);
ASSERT tokenizer.GetNextToken() IS "1";
ASSERT tokenizer.GetNextToken() IS "2";
ASSERT tokenizer.GetNextToken() IS "test";
ASSERT tokenizer.GetNextToken() IS "bar";
ASSERT tokenizer.HasMoreTokens() IS FALSE;
}
Test dei componenti privati tramite un'interfaccia pubblica ed evitando la duplicazione del test
Anche se non pensi di poter scomporre il tuo problema in un minor numero di componenti modulari (che puoi provare il 95% delle volte se provi a farlo), puoi semplicemente testare le funzioni private attraverso un'interfaccia pubblica. Molte volte i membri privati non valgono la pena testarli perché saranno testati attraverso l'interfaccia pubblica. Molte volte quello che vedo sono test che sembrano molto simili, ma testano due diverse funzioni / metodi. Quello che finisce per accadere è che quando i requisiti cambiano (e lo fanno sempre), ora hai 2 test non funzionanti invece di 1. E se davvero hai testato tutti i tuoi metodi privati, potresti avere più di 10 test rotti invece di 1. In breve , test delle funzioni private (usandoFRIEND_TEST
o renderli pubblici o utilizzare la riflessione) che potrebbero altrimenti essere testati tramite un'interfaccia pubblica può causare la duplicazione del test. Non lo vuoi davvero, perché niente fa più male della tua suite di test che ti rallenta. Dovrebbe ridurre i tempi di sviluppo e ridurre i costi di manutenzione! Se testate metodi privati altrimenti testati attraverso un'interfaccia pubblica, la suite di test potrebbe benissimo fare il contrario e aumentare attivamente i costi di manutenzione e aumentare i tempi di sviluppo. Quando rendi pubblica una funzione privata, o se usi qualcosa di simile FRIEND_TEST
e / o riflesso, di solito finirai per pentirti a lungo termine.
Considera la seguente possibile implementazione della Tokenizer
classe:
Supponiamo che SplitUpByDelimiter()
sia responsabile della restituzione di un array in modo tale che ogni elemento dell'array sia un token. Inoltre, diciamo solo che GetNextToken()
è semplicemente un iteratore su questo vettore. Quindi il tuo test pubblico potrebbe apparire così:
TEST_THAT(Tokenizer, canParseSpaceDelimtedTokens)
{
input_string = "1 2 test bar"
tokenizer = Tokenizer(input_string);
ASSERT tokenizer.GetNextToken() IS "1";
ASSERT tokenizer.GetNextToken() IS "2";
ASSERT tokenizer.GetNextToken() IS "test";
ASSERT tokenizer.GetNextToken() IS "bar";
ASSERT tokenizer.HasMoreTokens() IS false;
}
Facciamo finta di avere quello che Michael Feather chiama uno strumento a tentoni . Questo è uno strumento che ti consente di toccare le parti private di altre persone. Un esempio è FRIEND_TEST
da googletest o riflessione se la lingua lo supporta.
TEST_THAT(TokenizerTest, canGenerateSpaceDelimtedTokens)
{
input_string = "1 2 test bar"
tokenizer = Tokenizer(input_string);
result_array = tokenizer.SplitUpByDelimiter(" ");
ASSERT result.size() IS 4;
ASSERT result[0] IS "1";
ASSERT result[1] IS "2";
ASSERT result[2] IS "test";
ASSERT result[3] IS "bar";
}
Bene, ora diciamo che i requisiti cambiano e la tokenizzazione diventa molto più complessa. Decidi che un delimitatore di stringa semplice non è sufficiente e ti serve unDelimiter
classe per gestire il lavoro. Naturalmente, ci si aspetta che un test si interrompa, ma quel dolore aumenta quando si testano funzioni private.
Quando possono essere appropriati i test sui metodi privati?
Nel software non esiste "taglia unica". A volte va bene (e in realtà l'ideale) "infrangere le regole". Consiglio vivamente di non testare la funzionalità privata quando è possibile. Ci sono due situazioni principali quando penso che vada bene:
Ho lavorato a lungo con i sistemi legacy (motivo per cui sono un grande fan di Michael Feathers), e posso tranquillamente dire che a volte è semplicemente più sicuro testare solo la funzionalità privata. Può essere particolarmente utile per ottenere "test di caratterizzazione" nella baseline.
Sei di fretta e devi fare la cosa più veloce possibile per qui e ora. A lungo termine, non si desidera testare metodi privati. Ma dirò che di solito ci vuole un po 'di tempo per refactoring per affrontare i problemi di progettazione. E a volte devi spedire in una settimana. Va bene: fai il veloce e sporco e prova i metodi privati usando uno strumento di brancolio se è quello che pensi sia il modo più veloce e affidabile per fare il lavoro. Ma capisci che quello che hai fatto è stato non ottimale nel lungo periodo, e per favore considera di tornarci (o, se è stato dimenticato ma lo vedi in seguito, risolvilo).
Probabilmente ci sono altre situazioni in cui va bene. Se pensi che vada bene e hai una buona giustificazione, allora fallo. Nessuno ti sta fermando. Basta essere consapevoli dei potenziali costi.
La scusa del TDD
A parte questo, non mi piacciono davvero le persone che usano TDD come scusa per testare metodi privati. Pratico TDD e non credo che TDD ti costringa a farlo. È possibile scrivere prima il test (per l'interfaccia pubblica) e quindi scrivere il codice per soddisfare tale interfaccia. A volte scrivo un test per un'interfaccia pubblica e lo soddisferò anche scrivendo uno o due metodi privati più piccoli (ma non collaudo direttamente i metodi privati, ma so che funzionano o il mio test pubblico fallirebbe ). Se ho bisogno di testare casi limite di quel metodo privato, scriverò un sacco di test che li colpiranno attraverso la mia interfaccia pubblica.Se non riesci a capire come colpire i casi limite, questo è un segnale forte che devi rifattorizzare in piccoli componenti ognuno con i propri metodi pubblici. È un segno che le tue funzioni private stanno facendo troppo e al di fuori dell'ambito della classe .
Inoltre, a volte trovo che scrivo un test che è troppo grande per essere morso al momento, e quindi penso "eh tornerò a quel test più tardi quando avrò più di un'API con cui lavorare" (I lo commenterò e lo terrò nella parte posteriore della mia mente). È qui che molti sviluppatori che ho incontrato inizieranno a scrivere test per la loro funzionalità privata, usando TDD come capro espiatorio. Dicono "oh, beh, ho bisogno di qualche altro test, ma per scrivere quel test, avrò bisogno di questi metodi privati. Pertanto, dal momento che non posso scrivere alcun codice di produzione senza scrivere un test, ho bisogno di scrivere un test per un metodo privato ". Ma quello che devono davvero fare è il refactoring in componenti più piccoli e riutilizzabili invece di aggiungere / testare un mucchio di metodi privati nella loro classe attuale.
Nota:
Ho risposto a una domanda simile sul test di metodi privati utilizzando GoogleTest qualche tempo fa. Ho principalmente modificato quella risposta per essere più agnostico della lingua qui.
PS Ecco la lezione pertinente sulle lezioni di iceberg e gli strumenti per tentare di Michael Feathers: https://www.youtube.com/watch?v=4cVZvoFGJTU