TDD: deridere oggetti strettamente accoppiati


10

A volte gli oggetti devono solo essere strettamente accoppiati. Ad esempio, CsvFileprobabilmente una classe dovrà lavorare strettamente con la CsvRecordclasse (o l' ICsvRecordinterfaccia).

Tuttavia, da quello che ho imparato in passato, uno dei principi principali dello sviluppo guidato dai test è "Non testare più di una classe alla volta". Significa che dovresti usare ICsvRecordbeffe o tronconi piuttosto che esempi reali di CsvRecord.

Tuttavia, dopo aver provato questo approccio, ho notato che deridere la CsvRecordclasse può diventare un po 'peloso. Il che mi porta a una delle due conclusioni:

  1. È difficile scrivere test unitari! È un odore di codice! Refactor!
  2. Deridere ogni singola dipendenza è semplicemente irragionevole.

Quando ho sostituito le mie derisioni con CsvRecordcasi reali , le cose sono andate molto più agevolmente. Quando ho cercato i pensieri degli altri, mi sono imbattuto in questo post del blog , che sembra supportare il n. 2 sopra. Per gli oggetti che sono naturalmente strettamente accoppiati, non dovremmo preoccuparci troppo del deridere.

Sono fuori strada? Ci sono degli svantaggi nell'ipotesi n. 2 sopra? Dovrei davvero pensare al refactoring del mio design?


1
Penso che sia un'idea sbagliata comune che l '"unità" nei "test unitari" debba necessariamente essere una classe. Penso che il tuo esempio mostri un caso in cui potrebbe essere meglio che quelle due classi formino un'unità. Ma non fraintendermi, concordo pienamente con la risposta di Robert Harvey.
Doc Brown,

Risposte:


11

Se hai davvero bisogno di coordinamento tra queste due classi, scrivi una CsvCoordinatorclasse che incapsuli le tue due classi e testala.

Tuttavia, contesto l'idea che CsvRecordnon è verificabile in modo indipendente. CsvRecordè fondamentalmente una classe DTO , no? È solo una raccolta di campi, con forse un paio di metodi di supporto. E CsvRecordpuò essere utilizzato anche in altri contesti CsvFile; puoi avere una collezione o un array di CsvRecords, per esempio.

CsvRecordPrima prova . Assicurarsi che superi tutti i suoi test. Quindi, vai avanti e usalo CsvRecordcon la tua CsvFileclasse durante il test. Usalo come stub / mock pre-testato; riempilo con i dati di test pertinenti, passali a CsvFilee scrivi i tuoi casi di test a tale scopo.


1
Sì, CsvRecord è sicuramente testabile indipendentemente. Il problema è che se qualcosa si interrompe in CsvRecord, i test CsvData falliranno. Ma non penso che sia un grosso problema.
Phil

1
Penso che tu voglia che ciò accada. :)
Robert Harvey,

1
@RobertHarvey: in teoria, potrebbe diventare un problema se CsvRecord e CsvFile stanno diventando classi abbastanza complesse e se un test si interrompe per CsvFile, ora non si sa immediatamente se si tratta di un problema in CsvFile o CsvRecord. Ma suppongo che sia più un caso ipotetico: se avessi il compito di programmare tali classi per un programma del mondo reale, lo farei esattamente come lo descrivi.
Doc Brown,

2
@Phil: Se si CsvRecordrompe, ovviamente CsvDatafallisce; ma va bene, perché test CsvRecordprima, e se fallisce, i CsvFiletest non hanno senso. Puoi ancora distinguere tra errori in CsvRecorde in CsvFile.
martedì

5

Il motivo del test di una classe alla volta è che non si desidera che i test per una classe abbiano dipendenze dal comportamento di una seconda classe. Ciò significa che se il tuo test per la Classe A esercita una delle funzionalità della Classe B, allora dovresti deridere la Classe B per rimuovere la dipendenza da particolari funzionalità all'interno della Classe B.

Una classe come CsvRecordmi sembra che sia principalmente per l'archiviazione dei dati - non è una classe con troppe funzionalità proprie. Cioè, può avere costruttori, getter, setter, ma nessun metodo con una logica sostanziale reale. Certo, sto indovinando qui - forse hai scritto una classe chiamata CsvRecordche fa numerosi calcoli complessi.

Ma se CsvRecordnon ha una vera logica propria, non c'è niente da guadagnare deridendolo. Questa è davvero solo la vecchia massima: "non deridere oggetti di valore" .

Quindi, quando si considera se deridere una determinata classe (per un test di una classe diversa), è necessario tenere conto di quanta parte della propria logica ha quella classe e quanta parte di quella logica verrà eseguita nel corso del test.


+1. Qualsiasi test il cui esito dipende dalla correttezza di più comportamenti di un oggetto è un test di integrazione, non un test unitario. Devi prendere in giro uno di questi oggetti per ottenere un vero test unitario. Ciò non si applica agli oggetti che non presentano alcun comportamento reale, ad esempio solo con getter e setter.
guillaume31,

1

Il n. 2 va bene. Le cose possono essere e dovrebbero essere strettamente accoppiate se i loro concetti sono strettamente accoppiati. Questo dovrebbe essere raro e generalmente evitato, ma nell'esempio fornito ha senso.


0

Le classi "accoppiate" sono reciprocamente dipendenti l'una dall'altra. Questo non dovrebbe essere il caso di ciò che stai descrivendo: un CsvRecord non dovrebbe davvero preoccuparsi del CsvFile che lo contiene, quindi la dipendenza va solo in un modo. Va bene, e non è un accoppiamento stretto.

Dopotutto, se una classe contiene la variabile String name, non diresti che è strettamente accoppiata a String, vero?

Quindi, l'unità testa il CsvRecord per il comportamento desiderato.

Quindi usa un framework beffardo (Mockito è fantastico) per verificare se la tua unità sta interagendo con gli oggetti da cui dipende correttamente. Il comportamento che vuoi testare, davvero - è che CsvFile gestisce CsvRcords nel modo previsto. Il funzionamento interno di CvsRecord non dovrebbe importare: è come CvsFile comunica con esso.

Infine, TDD non riguarda solo i test unitari. Puoi certamente (e dovresti) iniziare con test funzionali che esaminano il comportamento funzionale di come funzionano i componenti più grandi, ovvero la storia o lo scenario dell'utente. I test delle unità impostano le aspettative e verificano i pezzi, i test funzionali fanno lo stesso per l'intero.


1
-1, l'accoppiamento stretto non significa necessariamente dipendenze cicliche, è un'idea sbagliata. Nell'esempio, CsvFile è strettamente accoppiato a CsvRecord(ma non viceversa). L'OP chiede se è una buona idea testare CsvFiledisaccoppiandolo CsvRecordtramite un ICsvRecord, non viceversa.
Doc Brown,

2
@DocBrown: se l'accoppiamento è stretto o no CsvFiledipende da quanto dipende dal funzionamento interno CsvRecord, cioè dalla quantità di ipotesi che il file ha sul record. Le interfacce aiutano a documentare e applicare tali assunzioni (o piuttosto l'assenza di altre assunzioni), ma la quantità di accoppiamento rimane la stessa, tranne che con un'interfaccia, è possibile agganciare una diversa classe di record CsvFile. Presentare l'interfaccia solo per poter dire che hai ridotto l'accoppiamento è sciocco.
martedì

0

Ci sono davvero due domande qui. Il primo è se esistono situazioni in cui non è consigliabile prendere in giro un oggetto. Questo è senza dubbio vero, come dimostrato dalle altre eccellenti risposte. La seconda domanda è se il tuo caso particolare è una di quelle situazioni. Su questa domanda non sono convinto.

Probabilmente il motivo più comune per non deridere una classe è se si tratta di una classe di valore. Tuttavia, devi guardare il motivo dietro la regola. Non è perché la classe derisa sarà in qualche modo cattiva, è perché sarà essenzialmente identica all'originale. In tal caso, il test dell'unità non sarebbe più semplice utilizzando la classe originale.

È molto probabile che il tuo codice sia una delle rare eccezioni in cui il refactoring non sarebbe di aiuto, ma dovresti dichiararlo tale solo dopo che gli sforzi diligenti di refactoring non hanno funzionato. Anche gli sviluppatori esperti possono avere difficoltà a vedere alternative al proprio design. Se non riesci a pensare a un modo possibile per migliorarlo, chiedi a qualcuno esperto di dargli una seconda occhiata.

Molte persone sembrano presumere che la tua CsvRecordsia una classe di valore. Prova a farne uno. Rendilo immutabile se puoi. Se hai due oggetti con puntatori l'uno all'altro, rimuovine uno e scopri come farlo funzionare. Cerca luoghi in cui dividere classi e funzioni. Il posto migliore per dividere una classe potrebbe non corrispondere sempre al layout fisico del file. Prova a invertire la relazione padre / figlio delle classi. Forse hai bisogno di una classe separata per leggere e scrivere file CSV. Forse hai bisogno di classi separate per gestire l'I / O dei file e l'interfaccia ai livelli superiori. Ci sono un sacco di cose da provare prima di dichiararlo irrilevante.

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.