Dovresti interrompere la lezione in questione.
Ogni classe dovrebbe svolgere alcune semplici attività. Se l'attività è troppo complicata per essere testata, l'attività svolta dalla classe è troppo grande.
Ignorando la sciocchezza di questo design:
class NewYork
{
decimal GetRevenue();
decimal GetExpenses();
decimal GetProfit();
}
class Miami
{
decimal GetRevenue();
decimal GetExpenses();
decimal GetProfit();
}
class MyProfit
{
MyProfit(NewYork new_york, Miami miami);
boolean bothProfitable();
}
AGGIORNARE
Il problema con i metodi di stubbing in una classe è che stai violando l'incapsulamento. Il test dovrebbe verificare se il comportamento esterno dell'oggetto corrisponde o meno alle specifiche. Qualunque cosa accada all'interno dell'oggetto non è affar suo.
Il fatto che FullName utilizzi FirstName e LastName è un dettaglio dell'implementazione. Nulla al di fuori della classe dovrebbe interessarsi a ciò che è vero. Deridendo i metodi pubblici per testare l'oggetto, si parte dal presupposto che tale oggetto sia implementato.
Ad un certo punto in futuro, tale ipotesi potrebbe smettere di essere corretta. Forse tutta la logica dei nomi verrà spostata in un oggetto Nome che la Persona chiama semplicemente. Forse FullName accederà direttamente alle variabili membro first_name e last_name anziché chiamare FirstName e LastName.
La seconda domanda è perché senti il bisogno di farlo. Dopo tutto la tua classe personale potrebbe essere testata qualcosa del tipo:
Person person = new Person("John", "Doe");
Test.AssertEquals(person.FullName(), "John Doe");
Non dovresti sentire la necessità di stub nulla per questo esempio. Se lo fai, allora sei contento e beh ... smettila! Non c'è alcun vantaggio nel deridere i metodi lì perché hai comunque il controllo su ciò che è in essi.
L'unico caso in cui sembrerebbe avere senso per i metodi che FullName utilizza per essere deriso è se in qualche modo FirstName () e LastName () fossero operazioni non banali. Forse stai scrivendo uno di quei generatori di nomi casuali, oppure FirstName e LastName interrogano il database per la risposta, o qualcosa del genere. Ma se è quello che succede, suggerisce che l'oggetto sta facendo qualcosa che non appartiene alla classe Person.
In altre parole, deridere i metodi è prendere l'oggetto e spezzarlo in due pezzi. Un pezzo viene deriso mentre l'altro è in fase di test. Quello che stai facendo è essenzialmente una frantumazione ad-hoc dell'oggetto. In tal caso, basta semplicemente suddividere l'oggetto.
Se la tua classe è semplice, non dovresti sentire il bisogno di deriderne i pezzi durante un test. Se la tua classe è abbastanza complessa da farti sentire il bisogno di deridere, allora dovresti suddividere la classe in pezzi più semplici.
AGGIORNARE ANCORA
Per come la vedo io, un oggetto ha un comportamento esterno e interno. Il comportamento esterno include valori restituiti chiamate in altri oggetti ecc. Ovviamente, qualsiasi cosa in quella categoria dovrebbe essere testata. (altrimenti cosa testeresti?) Ma il comportamento interno non dovrebbe davvero essere testato.
Ora il comportamento interno viene testato, perché è ciò che risulta nel comportamento esterno. Ma non scrivo test direttamente sul comportamento interno, solo indirettamente attraverso il comportamento esterno.
Se voglio testare qualcosa, immagino che dovrebbe essere spostato in modo che diventi un comportamento esterno. Ecco perché penso che se vuoi deridere qualcosa, dovresti dividere l'oggetto in modo che la cosa che vuoi deridere sia ora nel comportamento esterno degli oggetti in questione.
Ma che differenza fa? Se FirstName () e LastName () sono membri di un altro oggetto, cambia davvero il problema di FullName ()? Se decidiamo che è necessario prendere in giro FirstName e LastName aiuta davvero a trovarsi su un altro oggetto?
Penso che se usi il tuo approccio beffardo, allora crei una cucitura nell'oggetto. Hai funzioni come FirstName () e LastName () che comunicano direttamente con un'origine dati esterna. Hai anche FullName () che no. Ma dal momento che sono tutti nella stessa classe, ciò non è evidente. Alcuni pezzi non dovrebbero accedere direttamente all'origine dati e altri lo sono. Il tuo codice sarà più chiaro se solo dividi quei due gruppi.
MODIFICARE
Facciamo un passo indietro e chiediamo: perché deridiamo gli oggetti quando testiamo?
- Rendere i test eseguiti in modo coerente (evitare di accedere a cose che cambiano da una corsa all'altra)
- Evitare l'accesso a risorse costose (non accedere a servizi di terze parti, ecc.)
- Semplifica il sistema in prova
- Semplifica il test di tutti gli scenari possibili (ad es. Simulazione di guasti, ecc.)
- Evita di dipendere dai dettagli di altri pezzi di codice in modo che i cambiamenti in quegli altri pezzi di codice non interrompano questo test.
Ora, penso che i motivi 1-4 non si applichino a questo scenario. Deridere la fonte esterna durante il test del nome completo si prende cura di tutti quei motivi per deridere. L'unico pezzo non gestito che è la semplicità, ma sembra che l'oggetto sia abbastanza semplice da non preoccupare.
Penso che la tua preoccupazione sia la ragione numero 5. La preoccupazione è che ad un certo punto in futuro la modifica dell'implementazione di FirstName e LastName interromperà il test. In futuro, FirstName e LastName potrebbero ottenere i nomi da una posizione o fonte diversa. Ma FullName probabilmente lo sarà sempre FirstName() + " " + LastName()
. Ecco perché vuoi testare FullName deridendo FirstName e LastName.
Quello che hai quindi è un sottoinsieme dell'oggetto persona che è più probabile che cambi rispetto agli altri. Il resto dell'oggetto utilizza questo sottoinsieme. Quel sottoinsieme attualmente recupera i suoi dati utilizzando una fonte, ma potrebbe recuperarli in un modo completamente diverso in un secondo momento. Ma per me sembra che quel sottoinsieme sia un oggetto distinto che cerca di uscire.
Mi sembra che se deridi il metodo dell'oggetto, lo stai suddividendo. Ma lo stai facendo in modo ad hoc. Il tuo codice non chiarisce che ci sono due pezzi distinti all'interno dell'oggetto Person. Quindi dividi semplicemente l'oggetto nel tuo codice reale, in modo che sia chiaro dalla lettura del tuo codice cosa sta succedendo. Scegli la divisione effettiva dell'oggetto che ha senso e non provare a dividere l'oggetto in modo diverso per ogni test.
Ho il sospetto che potresti obiettare a dividere il tuo oggetto, ma perché?
MODIFICARE
Mi sbagliavo.
Dovresti dividere gli oggetti piuttosto che introdurre divisioni ad hoc deridendo i singoli metodi. Tuttavia, ero troppo concentrato sull'unico metodo di divisione degli oggetti. Tuttavia, OO offre più metodi per suddividere un oggetto.
Cosa proporrei:
class PersonBase
{
abstract sring FirstName();
abstract string LastName();
string FullName()
{
return FirstName() + " " + LastName();
}
}
class Person extends PersonBase
{
string FirstName();
string LastName();
}
class FakePerson extends PersonBase
{
void setFirstName(string);
void setLastName(string);
string getFirstName();
string getLastName();
}
Forse è quello che stavi facendo da sempre. Ma non credo che questo metodo avrà i problemi che ho visto con i metodi beffardi perché abbiamo chiaramente delineato da che parte sta ciascun metodo. E usando l'ereditarietà, evitiamo l'imbarazzo che sorgerebbe se usassimo un oggetto wrapper aggiuntivo.
Ciò introduce un po 'di complessità, e solo per un paio di funzioni di utilità probabilmente le testerei semplicemente deridendo la fonte di terze parti sottostante. Certo, sono a maggior rischio di rottura ma non vale la pena riorganizzarli. Se hai un oggetto abbastanza complesso da doverlo dividere, penso che qualcosa del genere sia una buona idea.