Come testare le classi astratte: estendere con gli stub?


446

Mi chiedevo come testare le classi astratte e le classi che estendono le classi astratte.

Dovrei testare la classe astratta estendendola, eliminando i metodi astratti e quindi testando tutti i metodi concreti? Quindi testare solo i metodi che sovrascrivo e testare i metodi astratti nei test unitari per oggetti che estendono la mia classe astratta?

Dovrei avere un caso di test astratto che può essere usato per testare i metodi della classe astratta ed estendere questa classe nel mio caso di test per oggetti che estendono la classe astratta?

Nota che la mia classe astratta ha alcuni metodi concreti.

Risposte:


268

Scrivi un oggetto Mock e usalo solo per i test. Di solito sono molto, molto minimali (ereditano dalla classe astratta) e non di più, quindi nel tuo Unit Test puoi chiamare il metodo astratto che vuoi testare.

Dovresti testare una classe astratta che contiene una logica come tutte le altre classi che hai.


9
Dannazione, devo dire che questa è la prima volta che condivido l'idea di usare un finto.
Jonathan Allen il

5
Hai bisogno di due lezioni, una finta e una prova. La classe finta estende solo i metodi astratti della classe astratta sotto test. Tali metodi possono essere no-op, restituire null, ecc. In quanto non verranno testati. La classe test verifica solo l'API pubblica non astratta (ovvero l'interfaccia implementata dalla classe Abstract). Per qualsiasi classe che estende la classe Abstract, avrai bisogno di ulteriori classi di test perché i metodi astratti non sono stati coperti.
cyber-monk,

10
È ovviamente possibile farlo .. ma per testare veramente qualsiasi classe derivata testerai questa funzionalità di base più e più volte ... il che ti porta ad avere un dispositivo di prova astratto in modo da poter fattorizzare questa duplicazione nei test. Tutto questo profuma! Consiglio vivamente di dare un'altra occhiata al motivo per cui stai usando le classi astratte in primo luogo e vedere se qualcos'altro funzionerebbe meglio.
Nigel Thorne,

5
la risposta successiva troppo stimolata è molto meglio.
Martin Spamer,

22
@MartiSpamer: Non direi che questa risposta è sopravvalutata perché è stata scritta molto prima (2 anni) rispetto alla risposta che ritieni migliore sotto. Incoraggiamo semplicemente Patrick perché nel contesto di quando ha pubblicato questa risposta, è stato grandioso. Incoraggiamoci l'un l'altro. Saluti
Marvin Thobejane,

449

Esistono due modi in cui vengono utilizzate le classi di base astratte.

  1. Stai specializzando il tuo oggetto astratto, ma tutti i client useranno la classe derivata attraverso la sua interfaccia di base.

  2. Stai usando una classe base astratta per fattorizzare la duplicazione all'interno degli oggetti nel tuo progetto e i clienti usano le implementazioni concrete attraverso le proprie interfacce.!


Soluzione per 1 - Modello di strategia

Opzione 1

Se hai la prima situazione, allora hai effettivamente un'interfaccia definita dai metodi virtuali nella classe astratta che le tue classi derivate stanno implementando.

Dovresti considerare di renderla una vera interfaccia, cambiare la tua classe astratta in concreta e prendere un'istanza di questa interfaccia nel suo costruttore. Le classi derivate diventano quindi implementazioni di questa nuova interfaccia.

Imotore

Ciò significa che ora puoi testare la tua classe precedentemente astratta usando un'istanza finta della nuova interfaccia e ogni nuova implementazione attraverso l'interfaccia ora pubblica. Tutto è semplice e testabile.


Soluzione per 2

Se hai la seconda situazione, allora la tua classe astratta sta lavorando come classe di aiuto.

AbstractHelper

Dai un'occhiata alla funzionalità che contiene. Verifica se uno qualsiasi di essi può essere inserito negli oggetti che vengono manipolati per ridurre al minimo questa duplicazione. Se hai ancora qualcosa, cerca di renderlo una classe di supporto che la tua implementazione concreta prende nel loro costruttore e rimuove la loro classe di base.

Motor Helper

Questo porta di nuovo a classi concrete che sono semplici e facilmente verificabili.


Come regola

Favorire una rete complessa di oggetti semplici su una semplice rete di oggetti complessi.

La chiave per un codice testabile estensibile sono i piccoli blocchi costitutivi e il cablaggio indipendente.


Aggiornato: come gestire le miscele di entrambi?

È possibile avere una classe base che svolge entrambi questi ruoli ... vale a dire: ha un'interfaccia pubblica e ha metodi di supporto protetti. In questo caso, puoi scomporre i metodi di supporto in una classe (scenario2) e convertire l'albero di ereditarietà in un modello di strategia.

Se scopri di avere alcuni metodi che la tua classe base implementa direttamente e altri sono virtuali, allora puoi comunque convertire l'albero ereditario in un modello di strategia, ma lo prenderei anche come un buon indicatore che le responsabilità non sono allineate correttamente, e potrebbe bisogno di refactoring.


Aggiornamento 2: Classi astratte come trampolino di lancio (2014/06/12)

L'altro giorno ho avuto una situazione in cui ho usato l'abstract, quindi mi piacerebbe esplorare il perché.

Abbiamo un formato standard per i nostri file di configurazione. Questo particolare strumento ha 3 file di configurazione tutti in quel formato. Volevo una classe fortemente tipizzata per ogni file di impostazione in modo che, tramite l'iniezione delle dipendenze, una classe potesse chiedere le impostazioni a cui teneva.

Ho implementato questo avendo una classe base astratta che sa come analizzare i formati dei file delle impostazioni e le classi derivate che esponevano quegli stessi metodi, ma incapsulava la posizione del file delle impostazioni.

Avrei potuto scrivere un "SettingsFileParser" che le 3 classi includevano e poi delegare alla classe base per esporre i metodi di accesso ai dati. Ho scelto di non farlo ancora perché porterebbe a 3 classi derivate con più codice di delega in esse di ogni altra cosa.

Tuttavia ... man mano che questo codice si evolve e i consumatori di ciascuna di queste classi di impostazioni diventano più chiari. Ogni impostazione gli utenti chiederanno alcune impostazioni e le trasformano in qualche modo (poiché le impostazioni sono di testo, possono essere avvolte in oggetti per convertirle in numeri, ecc.). In questo caso inizierò ad estrarre questa logica nei metodi di manipolazione dei dati e li riporterò sulle classi di impostazioni fortemente tipizzate. Ciò porterà a un'interfaccia di livello superiore per ogni set di impostazioni, che alla fine non è più consapevole di avere a che fare con "impostazioni".

A questo punto le classi di impostazioni fortemente tipizzate non avranno più bisogno dei metodi "getter" che espongono l'implementazione di "impostazioni" sottostante.

A quel punto non vorrei più che la loro interfaccia pubblica includesse i metodi di accesso alle impostazioni; quindi cambierò questa classe per incapsulare una classe parser di impostazioni invece di derivarne.

La classe astratta è quindi: un modo per me di evitare il codice di delega al momento e un indicatore nel codice per ricordarmi di cambiare il design in seguito. Non potrei mai raggiungerlo, quindi potrebbe vivere un bel po '... solo il codice può dirlo.

Trovo che questo sia vero con qualsiasi regola ... come "nessun metodo statico" o "nessun metodo privato". Indicano un odore nel codice ... e va bene. Ti tiene alla ricerca dell'astrazione che ti sei perso ... e ti consente di continuare a fornire valore ai tuoi clienti nel frattempo.

Immagino regole come questa che definiscono un paesaggio, in cui il codice gestibile vive nelle valli. Quando aggiungi un nuovo comportamento, è come l'atterraggio della pioggia sul tuo codice. Inizialmente lo metti ovunque atterra .. poi rifatti per consentire alle forze del buon design di spingere il comportamento fino a quando tutto finisce nelle valli.


18
Questa è un'ottima risposta Molto meglio del più votato. Ma poi suppongo che solo coloro che vogliono davvero scrivere un codice testabile lo apprezzerebbero .. :)
MalcomTucker,

22
Non riesco a capire quanto sia buona una risposta. Ha completamente cambiato il mio modo di pensare alle lezioni astratte. Grazie Nigel.
MalcomTucker,

4
Oh no .. un altro principio che sto per ripensare! Grazie (sia sarcasticamente per ora, sia non sarcasticamente per una volta l'ho assimilato e mi sento un programmatore migliore)
Martin Lyne

11
Bella risposta. Sicuramente qualcosa a cui pensare ... ma ciò che stai dicendo non si riduce sostanzialmente a non usare le classi astratte?
Brianestey,

32
+1 solo per la Regola, "Favorisci una rete complessa di oggetti semplici su una semplice rete di oggetti complessi".
David Glass,

12

Quello che faccio per le classi e le interfacce astratte è il seguente: scrivo un test, che usa l'oggetto in quanto è concreto. Ma la variabile di tipo X (X è la classe astratta) non è impostata nel test. Questa classe di test non viene aggiunta alla suite di test, ma a sottoclassi di essa, che hanno un metodo di installazione che imposta la variabile su un'implementazione concreta di X. In questo modo non duplico il codice di test. Le sottoclassi del test non utilizzato possono aggiungere altri metodi di test, se necessario.


questo non causa problemi di casting nella sottoclasse? se X ha il metodo a e Y eredita X ma ha anche un metodo b. Quando si esegue la sottoclasse della classe di test, non è necessario eseguire il cast della variabile astratta su una Y per eseguire i test su b?
Johnno Nolan,

8

Per effettuare un test unitario specifico sulla classe astratta, è necessario derivarlo a scopo di test, testare i risultati base.method () e il comportamento previsto durante l'ereditarietà.

Testate un metodo chiamandolo, quindi testate una classe astratta implementandola ...


8

Se la tua classe astratta contiene funzionalità concrete che hanno un valore aziendale, di solito le proverò direttamente creando un doppio di prova che stuba i dati astratti o usando un framework beffardo per fare questo per me. Quale scelgo dipende molto dal fatto che ho bisogno di scrivere implementazioni specifiche per i test dei metodi astratti o meno.

Lo scenario più comune in cui devo fare questo è quando sto usando il modello Metodo modello , come quando sto costruendo una sorta di framework estensibile che verrà utilizzato da una terza parte. In questo caso, la classe astratta è ciò che definisce l'algoritmo che voglio testare, quindi ha più senso testare la base astratta che un'implementazione specifica.

Tuttavia, penso che sia importante che questi test si concentrino solo sulle concrete implementazioni della vera logica aziendale ; non dovresti avere i dettagli di implementazione dei test unitari della classe astratta perché finirai con dei test fragili.


6

un modo è quello di scrivere un caso di test astratto corrispondente alla tua classe astratta, quindi scrivere casi di test concreti che sottoclassino il tuo caso di test astratto. fallo per ogni sottoclasse concreta della tua classe astratta originale (ovvero la gerarchia del tuo caso di test rispecchia la tua gerarchia di classi). vedere Test di un'interfaccia nel libro delle ricette di junit: http://safari.informit.com/9781932394238/ch02lev1sec6 .

vedi anche Testcase Superclass nei modelli xUnit: http://xunitpatterns.com/Testcase%20Superclass.html


4

Discuterei contro i test "astratti". Penso che un test sia un'idea concreta e non abbia un'astrazione. Se hai elementi comuni, inseriscili in metodi o classi di supporto che tutti possano usare.

Per quanto riguarda il test di una classe di test astratta, assicurati di chiederti cosa stai testando. Esistono diversi approcci e dovresti scoprire cosa funziona nel tuo scenario. Stai provando a provare un nuovo metodo nella tua sottoclasse? Quindi i tuoi test interagiscono solo con quel metodo. Stai testando i metodi nella tua classe base? Quindi probabilmente avrà un dispositivo separato solo per quella classe e testerà ogni metodo individualmente con tutti i test necessari.


Non volevo ripetere il test del codice che avevo già testato, ecco perché stavo percorrendo la strada astratta del test case. Sto cercando di testare tutti i metodi concreti nella mia classe astratta in un unico posto.
Paul Whelan,

7
Non sono d'accordo con l'estrazione di elementi comuni nelle classi di supporto, almeno in alcuni (molti?) Casi. Se una classe astratta contiene alcune funzionalità concrete, penso che sia perfettamente accettabile testare direttamente tale funzionalità.
Seth Petry-Johnson,

4

Questo è lo schema che di solito seguo quando installo un cablaggio per testare una classe astratta:

public abstract class MyBase{
  /*...*/
  public abstract void VoidMethod(object param1);
  public abstract object MethodWithReturn(object param1);
  /*,,,*/
}

E la versione che utilizzo sotto test:

public class MyBaseHarness : MyBase{
  /*...*/
  public Action<object> VoidMethodFunction;
  public override void VoidMethod(object param1){
    VoidMethodFunction(param1);
  }
  public Func<object, object> MethodWithReturnFunction;
  public override object MethodWithReturn(object param1){
    return MethodWihtReturnFunction(param1);
  }
  /*,,,*/
}

Se i metodi astratti vengono chiamati quando non me lo aspetto, i test falliscono. Quando organizzo i test, posso facilmente eliminare i metodi astratti con lambda che eseguono asserzioni, generano eccezioni, restituiscono valori diversi, ecc.


3

Se i metodi concreti invocano uno dei metodi astratti, quella strategia non funzionerà e vorresti testare separatamente ogni comportamento della classe figlio. Altrimenti, estenderlo e cancellare i metodi astratti come hai descritto dovrebbe andare bene, sempre a condizione che i metodi concreti della classe astratta siano separati dalle classi secondarie.


2

Suppongo che potresti voler testare la funzionalità di base di una classe astratta ... Ma probabilmente saresti meglio estendere la classe senza sovrascrivere alcun metodo e fare beffe del minimo sforzo per i metodi astratti.


2

Una delle principali motivazioni per l'utilizzo di una classe astratta è quella di abilitare il polimorfismo all'interno dell'applicazione, ovvero: è possibile sostituire una versione diversa in fase di esecuzione. In realtà, questo è molto simile all'utilizzo di un'interfaccia, tranne la classe astratta che fornisce un impianto idraulico comune, spesso indicato come modello di modello .

Dal punto di vista del test unitario, ci sono due cose da considerare:

  1. Interazione della tua classe astratta con le classi correlate . L'uso di un framework di test simulato è l'ideale per questo scenario in quanto mostra che la tua classe astratta gioca bene con gli altri.

  2. Funzionalità delle classi derivate . Se disponi di una logica personalizzata che hai scritto per le tue classi derivate, dovresti testarle separatamente.

modifica: RhinoMocks è un fantastico framework di test di simulazione che può generare oggetti simulati in fase di esecuzione derivando dinamicamente dalla tua classe. Questo approccio può farti risparmiare innumerevoli ore di classi derivate con codifica manuale.


2

In primo luogo se la classe astratta conteneva un metodo concreto, penso che dovresti farlo considerando questo esempio

 public abstract class A 

 {

    public boolean method 1
    {
        // concrete method which we have to test.

    }


 }


 class B extends class A

 {

      @override
      public boolean method 1
      {
        // override same method as above.

      }


 } 


  class Test_A 

  {

    private static B b;  // reference object of the class B

    @Before
    public void init()

      {

      b = new B ();    

      }

     @Test
     public void Test_method 1

       {

       b.method 1; // use some assertion statements.

       }

   }

1

Se una classe astratta è appropriata per la tua implementazione, prova (come suggerito sopra) una classe concreta derivata. I tuoi presupposti sono corretti.

Per evitare confusioni future, tenere presente che questa classe di test concreti non è un finto, ma un falso .

In termini rigorosi, una derisione è definita dalle seguenti caratteristiche:

  • Viene utilizzata una simulazione al posto di ogni singola dipendenza della classe soggetto da testare.
  • Una finta è una pseudo-implementazione di un'interfaccia (si può ricordare che, come regola generale, le dipendenze dovrebbero essere dichiarate come interfacce; la testabilità è una delle ragioni principali per questo)
  • I comportamenti dei membri dell'interfaccia del mock - siano essi metodi o proprietà - sono forniti al momento del test (di nuovo, usando un framework di derisione). In questo modo, si evita l'accoppiamento dell'implementazione testata con l'implementazione delle sue dipendenze (che dovrebbero avere tutti i propri test discreti).

1

Seguendo la risposta di @ patrick-desjardins, ho implementato l'abstract e la sua classe di implementazione insieme a @Testquanto segue:

Classe astratta - ABC.java

import java.util.ArrayList;
import java.util.List;

public abstract class ABC {

    abstract String sayHello();

    public List<String> getList() {
        final List<String> defaultList = new ArrayList<>();
        defaultList.add("abstract class");
        return defaultList;
    }
}

Poiché le classi astratte non possono essere istanziate, ma possono essere sottoclassate , la classe concreta DEF.java è la seguente:

public class DEF extends ABC {

    @Override
    public String sayHello() {
        return "Hello!";
    }
}

Classe @Test per testare sia il metodo astratto che quello non astratto:

import org.junit.Before;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.contains;
import java.util.Collection;
import java.util.List;
import static org.hamcrest.Matchers.equalTo;

import org.junit.Test;

public class DEFTest {

    private DEF def;

    @Before
    public void setup() {
        def = new DEF();
    }

    @Test
    public void add(){
        String result = def.sayHello();
        assertThat(result, is(equalTo("Hello!")));
    }

    @Test
    public void getList(){
        List<String> result = def.getList();
        assertThat((Collection<String>) result, is(not(empty())));
        assertThat(result, contains("abstract class"));
    }
}
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.