Lo spionaggio su una classe testata è una cattiva pratica?


13

Sto lavorando a un progetto in cui le chiamate interne di classe sono usuali ma i risultati sono molte volte valori semplici. Esempio ( non codice reale ):

public boolean findError(Set<Thing1> set1, Set<Thing2> set2) {
  if (!checkFirstCondition(set1, set2)) {
    return false;
  }
  if (!checkSecondCondition(set1, set2)) {
    return false;
  }
  return true;
}

Scrivere unit test per questo tipo di codice è davvero difficile in quanto voglio solo testare il sistema di condizioni e non l'implementazione delle condizioni reali. (Lo faccio in test separati.) In effetti sarebbe meglio se passassi funzioni che implementano le condizioni e nei test fornisco semplicemente un po 'di derisione. Il problema con questo approccio è la rumorosità: usiamo molto i generici .

Una soluzione funzionante; tuttavia, è rendere l'oggetto testato una spia e deridere le chiamate alle funzioni interne.

systemUnderTest = Mockito.spy(systemUnderTest);
doReturn(true).when(systemUnderTest).checkFirstCondition(....);

La preoccupazione qui è che l'implementazione del SUT sia effettivamente cambiata e potrebbe essere problematico mantenere i test sincronizzati con l'implementazione. È vero? Esistono buone pratiche per evitare questo caos delle chiamate interne al metodo?

Si noti che stiamo parlando di parti di un algoritmo, quindi suddividerlo in più classi potrebbe non essere una decisione desiderata.

Risposte:


15

I test unitari dovrebbero trattare le classi che testano come scatole nere. L'unica cosa che conta è che i suoi metodi pubblici si comportano nel modo previsto. Non importa come la classe ottenga questo attraverso metodi interni statali e privati.

Quando ritieni che sia impossibile creare test significativi in ​​questo modo, è un segno che le tue classi sono troppo potenti e fanno troppo. Dovresti considerare di spostare alcune delle loro funzionalità in classi separate che possono essere testate separatamente.


1
Ho capito l'idea di test unitari molto tempo fa e ne ho scritti molti con successo. Sta solo ingannando che qualcosa sembra semplice sulla carta, sembra peggiore nel codice, e finalmente mi trovo di fronte a qualcosa che ha un'interfaccia davvero semplice ma mi richiede di deridere metà del mondo attorno agli input.
allprog,

@allprog Quando devi fare un sacco di beffe, sembra che tu abbia troppe dipendenze tra le tue classi. Hai provato a ridurre l'accoppiamento tra loro?
Philipp,

@allprog se ti trovi in ​​quella situazione, la colpa è del design della classe.
itsbruce,

È il modello di dati che causa il mal di testa. Deve rispettare le regole ORM e molti altri requisiti. Con la pura logica aziendale e il codice stateless è molto più facile ottenere i test unitari corretti.
allprog,

3
I test unitari non devono necessariamente gestire SUT come backbox. Questo è il motivo per cui sono chiamati unit test. Deridendo le dipendenze posso influenzare l'ambiente e per sapere cosa devo deridere, devo conoscere anche alcuni degli interni. Ma ovviamente questo non significa che il SUT debba essere modificato in alcun modo. Lo spionaggio, tuttavia, consente di apportare alcune modifiche.
allprog,

4

Se sia findError()ed checkFirstCondition()ecc. Sono metodi pubblici della tua classe, allora findError()è effettivamente una facciata per funzionalità che è già disponibile dalla stessa API. Non c'è nulla di sbagliato in questo, ma significa che devi scrivere dei test molto simili a quelli già esistenti. Questa duplicazione riflette semplicemente la duplicazione nell'interfaccia pubblica. Questo non è un motivo per trattare questo metodo in modo diverso dagli altri.


I metodi interni sono resi pubblici solo perché devono essere testabili e non voglio sottoclassare SUT o includere i test unitari nella classe SUT come classe interna statica. Ma ho capito il tuo punto. Tuttavia, non sono riuscito a trovare buone linee guida per evitare questo tipo di situazioni. I tutorial sono sempre bloccati al livello base che non ha nulla a che fare con il software reale. Altrimenti, la ragione per lo spionaggio è esattamente per evitare la duplicazione del codice di test e rendere l'ambito dei test mirato.
allprog,

3
Non sono d'accordo sul fatto che i metodi di supporto debbano essere pubblici per test di unità adeguati. Se il contratto di un metodo afferma che verifica varie condizioni, allora non c'è nulla di sbagliato nello scrivere diversi test sullo stesso metodo pubblico, uno per ogni "subappalto". Lo scopo dei test unitari è ottenere la copertura di tutto il codice, non ottenere una copertura superficiale dei metodi pubblici tramite una corrispondenza metodo-test 1: 1.
Kilian Foth,

L'uso della sola API pubblica per i test è molte volte significativamente più complesso rispetto al test dei pezzi interni uno per uno. Non discuto, capisco che questo approccio non è il migliore e ha il senno di poi che la mia domanda mostra. Il problema maggiore è che le funzioni non sono componibili in Java e le soluzioni alternative sono estremamente concise. Ma non sembra esserci altra soluzione per test di unità reali.
allprog,

4

I test unitari dovrebbero testare il contratto; è l'unica cosa importante, per loro. Testare tutto ciò che non fa parte del contratto non è solo una perdita di tempo, è una potenziale fonte di errore. Ogni volta che vedi uno sviluppatore cambiare i test quando cambia un dettaglio di implementazione, le campane di allarme dovrebbero suonare; tale sviluppatore potrebbe nascondere (intenzionalmente o meno) i propri errori. Testare deliberatamente i dettagli dell'implementazione forza questa cattiva abitudine, rendendo più probabile che gli errori vengano mascherati.

Le chiamate interne sono un dettaglio di implementazione e dovrebbero interessare solo la misurazione delle prestazioni . Che di solito non è il compito dei test unitari.


Sembra fantastico. Ma in realtà, la "stringa" che devo digitare e chiamarlo codice è in un linguaggio che sa molto poco delle funzioni. In teoria posso facilmente descrivere un problema e fare delle sostituzioni qua e là per semplificarlo. Nel codice devo aggiungere molto rumore sintattico per ottenere questa flessibilità che mi rifiuta di usarlo. Se method acontiene una chiamata a method bnella stessa classe, allora i test di adevono includere i test di b. E non c'è modo di cambiarlo fintanto che bnon viene passato acome parametro Ma non c'è altra soluzione, vedo.
allprog,

1
Se bfa parte dell'interfaccia pubblica, dovrebbe comunque essere testato. In caso contrario, non è necessario testarlo. Se lo hai reso pubblico solo perché volevi provarlo, hai sbagliato.
itsbruce,

Vedi il mio commento alla risposta di @ Philip. Non ne ho ancora parlato, ma il modello di dati è la fonte del male. Il codice puro e senza stato è un gioco da ragazzi.
allprog,

2

In primo luogo, mi chiedo cosa sia difficile testare sulla funzione di esempio che hai scritto? Per quanto posso vedere, puoi semplicemente passare vari input e verificare che sia restituito il valore booleano corretto. Cosa mi sto perdendo?

Per quanto riguarda le spie, il tipo di cosiddetto test "white-box" che utilizza spie e derisioni è ordini di grandezza più lavoro da scrivere, non solo perché c'è molto più codice di test da scrivere, ma ogni volta che l'implementazione è modificato, è necessario modificare anche i test (anche se l'interfaccia rimane la stessa). E questo tipo di test è anche meno affidabile del test black-box, perché è necessario assicurarsi che tutto quel codice di test aggiuntivo sia corretto e mentre ci si può fidare che i test unitari black-box falliranno se non corrispondono all'interfaccia , non ci si può fidare di questo riguardo al ricorso eccessivo al codice perché a volte il test non verifica nemmeno molto codice reale, ma solo il controllo. Se i mock non sono corretti, è probabile che i test abbiano esito positivo, ma il codice è ancora rotto.

Chiunque abbia esperienza con i test in white box può dirti che sono un dolore nel culo da scrivere e mantenere. Insieme al fatto che sono meno affidabili, i test in white box sono semplicemente molto inferiori nella maggior parte dei casi.


Grazie per la nota. La funzione di esempio è che gli ordini di grandezza sono più semplici di qualsiasi cosa tu debba scrivere in un algoritmo complesso. In realtà, la domanda si rivela più simile: è problematico testare algoritmi con spie in più parti. Questo non è un codice con stato, tutto lo stato è separato in argomenti di input. Il problema è il fatto che voglio testare la complessa funzione nell'esempio senza dover fornire parametri sani per le funzioni secondarie.
allprog,

Con l'alba della programmazione funzionale in Java 8 questo è diventato leggermente più elegante, ma mantenere la funzionalità in una singola classe può essere una scelta migliore nel caso di algoritmi piuttosto che estrarre le diverse parti (da sole non utili) in "usa una volta" classi solo a causa della testabilità. A questo proposito le spie fanno lo stesso delle beffe ma senza dover far saltare visivamente il codice coerente. In realtà, viene utilizzato lo stesso codice di configurazione utilizzato per i mock. Mi piace stare lontano dagli estremi, ogni tipo di test può essere appropriato in luoghi specifici. Testare in qualche modo è molto meglio di niente. :)
allprog,

"Voglio testare la funzione complessa ... senza dover fornire parametri sani per le funzioni secondarie" - Non capisco cosa intendi lì. Quali sotto funzioni? Stai parlando delle funzioni interne utilizzate dalla "funzione complessa"?
BT,

Questo è ciò per cui lo spionaggio è utile nel mio caso. Le funzioni interne sono piuttosto complesse da controllare. Non a causa del codice ma perché implementano qualcosa di logicamente complesso. Spostare cose in una classe diversa è un'opzione naturale ma quelle funzioni da sole non sono affatto utili. Pertanto, tenere insieme la classe e controllarla tramite la funzionalità spia si è rivelata un'opzione migliore. Ha funzionato in modo impeccabile per quasi un anno e poteva facilmente resistere ai cambi di modello. Non ho mai usato questo modello da allora, ho pensato bene a dire che in alcuni casi è praticabile.
allprog,

@allprog "logicamente complesso" - Se è complesso, sono necessari test complessi. Non c'è modo di aggirare questo. Le spie renderanno tutto più difficile e complesso per te. Dovresti creare sotto-funzioni comprensibili che puoi testare da solo, piuttosto che usare spie per testare il loro comportamento speciale all'interno di un'altra funzione.
BT,
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.