Interfaccia tra codice dell'applicazione e test di unità

8

Sto lavorando a un progetto in cui dobbiamo implementare e testare le unità qualche nuovo modulo. Avevo in mente un'architettura abbastanza chiara, quindi ho rapidamente annotato la parte principale classi e metodi e poi abbiamo iniziato a scrivere test unitari.

Durante la scrittura dei test abbiamo dovuto apportare alcune modifiche a il codice originale, come

  • Rendere pubblici i metodi privati per testarli
  • Aggiunta di metodi aggiuntivi per accedere alle variabili private
  • Aggiunta di metodi aggiuntivi per iniettare oggetti fittizi che dovrebbero essere utilizzati quando il codice viene eseguito all'interno di un test unitario.

In qualche modo ho la sensazione che questi siano sintomi che stiamo facendo qualcosa di sbagliato, per esempio

  1. il progetto iniziale era sbagliato (alcune funzionalità dovrebbero essere state pubbliche dall'inizio),
  2. il codice non è stato progettato correttamente per essere interfacciato con i test di unità (forse perché abbiamo iniziato a progettare i test di unità quando già alcune classi erano state progettate),
  3. stiamo implementando i test unitari nel modo sbagliato (ad esempio, i test unitari dovrebbero solo testare / indirizzare direttamente i metodi pubblici di un'API, non quelli privati),
  4. una combinazione dei tre punti precedenti e forse alcuni problemi aggiuntivi a cui non ho pensato.

Dato che ho una certa esperienza con i test unitari ma sono lontano dall'essere un guru, sarei molto interessato a leggere i tuoi pensieri riguardo a questi problemi.

Oltre alle domande generali di cui sopra, ho alcune più specifiche, domande tecniche:

Domanda 1. Ha senso testare direttamente un metodo privato m di una classe A e persino renderlo pubblico per testarlo? O dovrei supporre che m sia indirettamente testato dai test unitari che coprono altri metodi pubblici che chiamano m?

Domanda 2. Se un'istanza della classe A contiene un'istanza di classe B (aggregazione composita), ha senso prendere in giro B per testare A? La mia prima idea era che non dovevo prendere in giro B perché l'istanza B faceva parte dell'istanza A, ma poi ho iniziato a dubitarne. Il mio argomento contro il beffardo B è lo stesso di 1: B è privato wrt A e usato solo per la sua implementazione, quindi beffardo B sembra come se io esponessi dettagli privati di A like in (1). Ma forse questi problemi indicano un difetto di progettazione: forse non dovremmo usare l'aggregazione composita ma una semplice associazione da A a B.

Domanda 3. Nell'esempio precedente, se decidiamo di prendere in giro B, come facciamo ad iniettare l'istanza B in A? Ecco alcune idee che abbiamo:

  • Inietti l'istanza B come argomento al costruttore A invece di creare l'istanza B nel costruttore A.
  • Passa un'interfaccia BFactory come argomento al costruttore A e lascia A utilizzare la factory per creare l'istanza B privata.
  • Utilizzare un singleton BFactory privato in A. Utilizzare un metodo statico A :: setBFactory () per impostare il singleton. Quando A vuole creare l'istanza B usa il singleton di fabbrica se è impostato (lo scenario di test), crea B direttamente se il singleton non è impostato (lo scenario del codice di produzione).

Le prime due alternative mi sembrano più pulite, ma richiedono un cambiamento la firma del costruttore A: cambiare l'API solo per renderlo più testabile mi sembra imbarazzante, è una pratica comune?

Il terzo ha il vantaggio che non richiede la modifica della firma del costruttore (la modifica all'API è meno invasiva), ma richiede il richiamo del metodo statico setBFactory () prima di iniziare il test, che è l'errore IMO -prone (dipendenza implicita da una chiamata di metodo affinché i test funzionino correttamente). Quindi non so quale scegliere.

    
posta Giorgio 28.05.2012 - 09:15
fonte

3 risposte

8

Penso che testare i metodi pubblici sia sufficiente la maggior parte del tempo.

Se hai una grande quantità di complessità nei tuoi metodi privati, allora considera di metterli in un'altra classe come metodi pubblici e usarli come chiamate private a quei metodi nella tua classe originale. In questo modo puoi garantire che entrambi i metodi nelle classi di utilità e originali funzionino correttamente.

Affidarsi in maniera massiccia ai metodi privati è un aspetto da considerare in merito alle design desicions.

    
risposta data 28.05.2012 - 10:26
fonte
5

Alla domanda 1: dipende da In genere, si inizia con i test unitari per i metodi pubblici. A volte si incontra un metodo che si desidera mantenere privato in A, ma si ritiene che sia opportuno testarlo anche separatamente. In questo caso, dovresti rendere pubblico m o rendere la classe di test TestA una classe friend di A . Ma attenzione, aggiungere un test unitario per testare m rende un po 'più difficile cambiare la firma o il comportamento di m in seguito; se vuoi mantenere un "dettaglio di implementazione" di A, potrebbe essere meglio non aggiungere un test di unità diretto.

Domanda 2: l'aggregazione composita (C ++ build-in) non funziona bene quando si tratta di simulare un'istanza. Infatti, poiché la costruzione di B avviene implicitamente nel costruttore di A, non hai la possibilità di iniettare la dipendenza dall'esterno. Se questo è un problema dipende dal modo in cui vuoi testare A: se pensi che abbia più senso testare A da solo, con una simulazione di B invece di B, meglio usare una semplice associazione. Se pensi di poter scrivere tutti i test unitari necessari per A senza prendere in giro B, allora un composito sarà probabilmente ok.

Domanda 3: cambiare un'API per rendere le cose più testabili è comune finché non si dispone di molto codice fino a quel momento basato su tale API. Quando si esegue TDD, non è necessario modificare l'API in un secondo momento per rendere le cose più testabili, inizialmente si inizia con un'API progettata per la testabilità. Se vuoi cambiare un'API in seguito per rendere le cose più testabili, potresti incontrare dei problemi, è vero. Quindi andrei con la prima o la seconda delle alternative che hai descritto purché tu possa cambiare la tua API senza dolore e usare qualcosa come la tua terza alternativa (nota: funziona anche senza il modello singleton) solo come ultima risorsa se devo non modificare l'API in nessun caso.

Riguardo alle tue preoccupazioni che potresti "sbagliare": ogni grande motore o macchina ha aperture di manutenzione, quindi IMHO la necessità di aggiungere qualcosa del genere al software non è troppo sorprendente.

    
risposta data 28.05.2012 - 10:41
fonte
1

Dovresti cercare Iniezione delle dipendenze e Inversione del controllo. Misko Hevery spiega molto sul suo blog. DI e IoC sono effettivamente un modello di progettazione per creare codice facile da testare e simulare.

Domanda 1: No, non rendere pubblici i metodi privati per testarli. Se il metodo è sufficientemente complesso, è possibile creare una classe collaboratore che contenga solo quel metodo e iniettarla (passare al costruttore) nell'altra classe. 1

Domanda 2: chi costruisce A in questo caso? Se si ha una classe factory / builder che costruisce A, non vi è alcun danno nel passare il collaboratore B nel costruttore. Se A è in un pacchetto / spazio dei nomi separato dal codice che lo utilizza, puoi addirittura rendere privato il pacchetto del costruttore e renderlo tale che factory / builder è l'unica classe in grado di costruirlo.

Domanda 3: Ho risposto a questa domanda alla domanda 2. Alcune note aggiuntive però:

  • L'uso di un modello di builder / factory ti consente di fare l'iniezione delle dipendenze quanto vuoi, senza doverti preoccupare di rendere il codice difficile da usare con la tua classe.
  • Separa tempo di costruzione dell'oggetto da tempo di utilizzo dell'oggetto , il che significa che il codice che utilizza l'API può essere più semplice.

1 Questa è la risposta C # / Java - C ++ potrebbe avere funzionalità extra che facilitano questo

Come risposta ai tuoi commenti:

Ciò che intendevo era che il tuo codice di produzione sarebbe stato modificato (per favore perdona il mio pseudo-C ++):

void MyClass::MyUseOfA()
{
  A* a = new A();
  a->SomeMethod();
}

A::A()
{
  m_b = new B();
}

a:

void MyClass::MyUseOfA()
{
  A* a = AFactory.NewA();
  a->SomeMethod();
}

A* AFactory::NewA()
{
  // Construct dependencies
  B* b = new B();
  return new A(b);
}

A::A(B* b)
{
  m_b = b;
}

Quindi il test può essere:

void MyTest::TestA()
{
  MockB* b = new MockB();
  b->SetSomethingInteresting(somethingInteresting);

  A* a = new A(b);

  a->DoSomethingInteresting();

  b->DidSomethingInterestingHappen();
}

In questo modo, non è necessario passare di fabbrica, il codice che chiama A non ha bisogno di sapere come costruire A, e il test può costruire A su misura per consentire il test del comportamento.

Nel tuo altro commento hai chiesto delle dipendenze nidificate. Quindi, ad esempio, se le tue dipendenze erano:

A -> C -> D -> B

La prima domanda da porsi è se A usa C e D. Se non lo sono, perché sono inclusi in A? Supponendo che vengano utilizzati, allora forse è necessario passare in C nella tua fabbrica e fare in modo che il tuo test costruisca un MockC che restituisca un MockB, permettendoti di testare tutte le possibili interazioni.

Se questo sta cominciando a complicarsi, potrebbe essere un segno che il tuo design è forse accoppiato troppo strettamente. Se è possibile allentare l'accoppiamento e mantenere alta la coesione, questo tipo di DI diventa più semplice da implementare.

    
risposta data 28.05.2012 - 12:01
fonte

Leggi altre domande sui tag