Va bene falsificare parte della classe in prova?


22

Supponiamo che io abbia una classe (perdona l'esempio inventato e il cattivo design di esso):

class MyProfit
{
  public decimal GetNewYorkRevenue();
  public decimal GetNewYorkExpenses();
  public decimal GetNewYorkProfit();

  public decimal GetMiamiRevenue();
  public decimal GetMiamiExpenses();
  public decimal GetMiamiProfit();

  public bool BothCitiesProfitable();

}

(Notare che i metodi GetxxxRevenue () e GetxxxExpenses () hanno dipendenze che vengono cancellate)

Ora sto testando l'unità BothCitiesProfitable () che dipende da GetNewYorkProfit () e GetMiamiProfit (). Va bene stub GetNewYorkProfit () e GetMiamiProfit ()?

Sembra che se non lo facessi, sto contemporaneamente testando GetNewYorkProfit () e GetMiamiProfit () insieme a BothCitiesProfitable (). Dovrei assicurarmi di impostare lo stub per GetxxxRevenue () e GetxxxExpenses () in modo che i metodi GetxxxProfit () restituiscano i valori corretti.

Finora ho visto solo esempi di dipendenze stubbing su classi esterne e non metodi interni.

E se va bene, c'è un modello particolare che dovrei usare per fare questo?

AGGIORNARE

Temo che potremmo perdere il problema principale e che probabilmente è colpa del mio scarso esempio. La domanda fondamentale è: se un metodo in una classe ha una dipendenza da un altro metodo esposto pubblicamente in quella stessa classe, va bene (o addirittura si consiglia) di stub quell'altro metodo?

Forse mi manca qualcosa, ma non sono sicuro che dividere la classe abbia sempre senso. Forse un altro esempio minimamente migliore sarebbe:

class Person
{
 public string FirstName()
 public string LastName()
 public string FullName()
}

dove il nome completo è definito come:

public string FullName()
{
  return FirstName() + " " + LastName();
}

Va bene stub FirstName () e LastName () durante il test di FullName ()?


Se testate un po 'di codice contemporaneamente, come può essere negativo? Qual è il problema? Normalmente, dovrebbe essere meglio testare il codice più spesso e più intensamente.
utente sconosciuto

Ho appena saputo del tuo aggiornamento, ho aggiornato la mia risposta
Winston Ewert,

Risposte:


27

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?

  1. Rendere i test eseguiti in modo coerente (evitare di accedere a cose che cambiano da una corsa all'altra)
  2. Evitare l'accesso a risorse costose (non accedere a servizi di terze parti, ecc.)
  3. Semplifica il sistema in prova
  4. Semplifica il test di tutti gli scenari possibili (ad es. Simulazione di guasti, ecc.)
  5. 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.


8
Ben detto. Se stai provando a trovare soluzioni alternative nei test, probabilmente dovrai riconsiderare il tuo progetto (come nota a margine, ora ho la voce di Frank Sinatra bloccata nella mia testa).
Problematico

2
"Deridendo i metodi pubblici per testare l'oggetto, si fa un'ipotesi su [come] quell'oggetto è implementato." Ma non è così ogni volta che metti un mozzicone a un oggetto? Ad esempio, supponiamo che io conosca il mio metodo xyz () che chiama qualche altro oggetto. Per testare xyz () in isolamento, devo stub l'altro oggetto. Pertanto il mio test deve conoscere i dettagli di implementazione del mio metodo xyz ().
Utente

Nel mio caso, i metodi "FirstName ()" e "LastName ()" sono semplici ma richiedono una API di terze parti per il loro risultato.
Utente

@Utente, aggiornato, probabilmente stai deridendo l'API di terze parti per testare FirstName e LastName. Cosa c'è di sbagliato nel fare lo stesso beffardo durante il test di FullName?
Winston Ewert,

@ Winston, questo è tutto il mio punto, al momento prendo in giro l'API di terze parti usato nel nome e nel cognome per testare il nome completo (), ma preferirei non preoccuparmi di come vengono implementati il ​​nome e il cognome durante il test del nome completo (ovviamente va bene quando sto testando nome e cognome). Da qui la mia domanda sul prendere in giro nome e cognome.
Utente

10

Sebbene io sia d'accordo con la risposta di Winston Ewert, a volte non è possibile interrompere una classe, sia a causa di vincoli di tempo, che l'API sia già utilizzata altrove o che cosa hai.

In un caso del genere, invece di metodi beffardi, scriverei i miei test per coprire la classe in modo incrementale. Testare i metodi getExpenses()e getRevenue(), quindi testare i getProfit()metodi, quindi testare i metodi condivisi. È vero che avrai più di un test che copre un particolare metodo, ma dal momento che hai scritto test di superamento per coprire i metodi individualmente, puoi essere sicuro che i tuoi risultati siano affidabili dato un input testato.


1
Concordato. Ma solo per sottolineare il punto per chiunque legga questo, questa è la soluzione che usi se non riesci a fare la soluzione migliore.
Winston Ewert,

5
@ Winston, e questa è la soluzione che usi prima di arrivare a una soluzione migliore. Cioè avendo una grande palla di codice legacy, devi coprirlo con test unitari prima di poterlo refactoring.
Péter Török,

@Péter Török, buon punto. Anche se penso che sia coperto da "non posso fare la soluzione migliore". :)
Winston Ewert,

@all Testare il metodo getProfit () sarà molto difficile poiché i diversi scenari per getExpenses () e getRevenues () moltiplicheranno effettivamente gli scenari necessari per getProfit (). se stiamo testando getExpenses () e otteniamo Revenues () in modo indipendente Non è una buona idea deridere questi metodi per testare getProfit ()?
Mohd Farid,

7

Per semplificare ulteriormente i tuoi esempi, supponi che stai testando C(), che dipende da A()e B(), ognuno dei quali ha le proprie dipendenze. IMO dipende da ciò che il tuo test sta cercando di raggiungere.

Se stai testando il comportamento di C()determinati comportamenti noti di A()e B()quindi è probabilmente più facile e meglio stub out A()e B(). Questo verrebbe probabilmente chiamato unit test dai puristi.

Se stai testando il comportamento dell'intero sistema (dal C()punto di vista), me ne andrei A()e B()come implementato e eliminerei le loro dipendenze (se ciò rende possibile il test) o impostare un ambiente sandbox, come un test Banca dati. Lo definirei un test di integrazione.

Entrambi gli approcci possono essere validi, quindi se è progettato in modo da poterlo testare in anticipo, probabilmente sarà meglio a lungo termine.

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.