Qual è l'approccio corretto per testare le classi con ereditarietà?

5

Supponendo che abbia la seguente struttura (eccessivamente semplificata):

class Base
{
  public:
    Base(int valueForFoo) : foo(valueForFoo) { };
    virtual ~Base() = 0;
    int doThings() { return foo; };
    int doOtherThings() { return 42; };

  protected:
    int foo;
}

class BarDerived : public Base
{
  public:
    BarDerived() : Base(12) { };
    ~BarDerived() { };
    int doBarThings() { return foo + 1; };
}

class BazDerived : public Base
{
  public:
    BazDerived() : Base(25) { };
    ~BazDerived() { };
    int doBazThings() { return 2 * foo; };
}

Come puoi vedere, la funzione doThings nella classe base restituisce risultati diversi in ogni classe derivata a causa dei diversi valori di foo , mentre il La funzione doOtherThings si comporta in modo identico su tutte le classi .

Quando desidero implementare i test unitari per queste classi, la gestione di doThings , doBarThings / doBazThings mi è chiara: devono essere coperti per ogni classe derivata. Ma come dovrebbe essere gestito doOtherThings ? È buona norma duplicare essenzialmente il caso di test in entrambe le classi derivate? Il problema peggiora se ci sono una mezza dozzina di funzioni come doOtherThings e più Classi derivate .

    
posta CharonX 17.04.2018 - 11:40
fonte

3 risposte

3

Nei tuoi test per BarDerived vuoi dimostrare che tutti i metodi (pubblici) di BarDerived funzionano correttamente (per le situazioni che hai testato). Allo stesso modo per BazDerived .

Il fatto che alcuni metodi siano implementati in una classe base non modifica questo obiettivo di test per BarDerived e BazDerived . Ciò porta alla conclusione che Base::doOtherThings deve essere testato sia nel contesto di BarDerived che BazDerived e che si ottengano test molto simili per quella funzione.

Il vantaggio di testare doOtherThings per ogni classe derivata è che se i requisiti per BarDerived cambiano in modo che BarDerived::doOtherThings debba restituire 24, allora il test fallito nel test BazDerived ti dice che potresti essere in rottura i requisiti di un'altra classe.

    
risposta data 17.04.2018 - 12:18
fonte
3

But how should doOtherThings be handled? Is it good practice to essentially duplicate the test case in both derived classes?

Normalmente mi aspetto che la Base abbia le sue specifiche, che puoi verificare per qualsiasi implementazione conforme, comprese le classi derivate.

void verifyBaseCompliance(const Base & systemUnderTest) {
    // checks that systemUnderTest conforms to the Base API
    // specification
}

void testBase () { verifyBaseCompliance(new Base()); }
void testBar () { verifyBaseCompliance(new BarDerived()); }
void testBaz () { verifyBaseCompliance(new BazDerived()); }
    
risposta data 17.04.2018 - 14:38
fonte
1

Hai un conflitto qui.

Vuoi testare il valore di ritorno di doThings() , che si basa su un valore letterale (valore const).

Ogni test che scrivi per questo è in grado di riassumere testare un valore const , che è privo di senso.

Per mostrarti un esempio più sensato (sono più veloce con C #, ma il principio è lo stesso)

public class TriplesYourInput : Base
{
    public TriplesYourInput(int input)
    {
        this.foo = 3 * input;
    }
}

Questa classe può essere testata in modo significativo:

var inputValue = 123;

var expectedOutputValue = inputValue * 3;
var receivedOutputValue = new TriplesYourInput(inputValue).doThings();

Assert.AreEqual(receivedOutputValue, expectedOutputValue);

Questo ha più senso da testare. Il suo output si basa sull'input che ha scelto per darlo. In tal caso, puoi assegnare a una classe un input scelto arbitrariamente, osservarne l'output e verificare se corrisponde alle tue aspettative.

Alcuni esempi di questo principio di prova. Si noti che i miei esempi hanno sempre un controllo diretto su quale sia l'input del metodo testabile.

  • Test if GetFirstLetterOfString() return "F" when I input "Flater".
  • Test if CountLettersInString() returns 6 when I input "Flater".
  • Test if ParseStringThatBeginsWithAnA() returns an exception when I input "Flater".

Tutti questi test possono inserire qualsiasi valore vogliano , a patto che le loro aspettative siano in linea con quello che stanno inserendo.

Ma se il tuo output è deciso da un valore costante, dovrai creare un'aspettativa costante, quindi testare se il primo corrisponde al secondo. Il che è sciocco, questo è sempre o mai che passerà; nessuno dei quali è un risultato significativo.

Alcuni esempi di questo principio di prova. Nota che questi esempi non hanno alcun controllo su almeno uno dei valori che vengono confrontati.

  • Test if Math.Pi == 3.1415...
  • Test if MyApplication.ThisConstValue == 123

Questi test per uno valore particolare. Se cambi questo valore, i tuoi test falliranno. In sostanza, non stai testando se la tua logica funziona per qualsiasi input valido, stai semplicemente testando se qualcuno è in grado di prevedere con precisione un risultato su cui non hanno alcun controllo.

Questo è essenzialmente il test della conoscenza dello scrittore di test della logica di business. Non sta testando il codice, ma lo stesso autore.

Ripristina il tuo esempio:

class BarDerived : public Base
{
  public:
    BarDerived() : Base(12) { };
    ~BarDerived() { };
    int doBarThings() { return foo + 1; };
}

Perché BarDerived ha sempre un foo uguale a 12 ? Qual è il significato di questo?

E visto che hai già deciso questo, cosa stai cercando di guadagnare scrivendo un test che conferma che BarDerived ha sempre un foo uguale a 12 ?

Questo diventa ancora peggio se inizi a prendere in considerazione che doThings() può essere sovrascritto in una classe derivata. Immagina se AnotherDerived debba sostituire doThings() in modo che restituisca sempre foo * 2 . Ora, avrai una classe che è hardcoded come Base(12) , il cui valore doThings() è 24. Sebbene tecnicamente testabile, è privo di qualsiasi significato contestuale. Il test non è comprensibile.

Non riesco davvero a pensare a un motivo per utilizzare questo approccio al valore codificato. Anche se esiste un caso di utilizzo valido, non capisco perché stai provando a scrivere un test per confermare questo valore codificato . Non c'è nulla da guadagnare testando se un valore costante è uguale allo stesso valore costante.

Qualsiasi errore di test dimostra che il test è sbagliato . Non vi è alcun risultato in cui un errore di test dimostra che la logica aziendale è sbagliata. In effetti, non sei in grado di confermare quali test sono stati creati per confermare in primo luogo.

Il problema non ha nulla a che fare con l'ereditarietà, nel caso ve lo stiate chiedendo. Devi solo accadere per aver usato un valore const nel costruttore della classe base, ma avresti potuto usare questo valore const altrove e quindi non sarebbe stato correlato a una classe ereditata.

Modifica

Ci sono casi in cui i valori hardcoded non sono un problema. (ancora, mi dispiace per la sintassi C # ma il principio è sempre lo stesso)

public class Base
{
    public int MultiplyFactor;
    protected int InitialValue;

    public Base(int value, int factor)
    {
        this.InitialValue = value;
        this.MultiplyFactor= factor;
    }

    public int GetMultipliedValue()
    {
         return this.InitialValue * this.MultiplyFactor;
    }
}

public class DoublesYourNumber : Base
{
    public DoublesYourNumber(int value) :  base(value, 2) {}
}

public class TriplesYourNumber : Base
{
    public TriplesYourNumber(int value) : base(value, 3) {}
}

Mentre il valore costante ( 2 / 3 ) sta ancora influenzando il valore di output di GetMultipliedValue() , il consumatore della tua classe ha ancora il controllo su di esso anche!

In questo esempio, è comunque possibile scrivere test significativi:

var inputValue = 123;

var expectedDoubledOutputValue = inputValue * 2;
var receivedDoubledOutputValue = new DoublesYourNumber(inputValue).GetMultipliedValue();

Assert.AreEqual(expectedDoubledOutputValue , receivedDoubledOutputValue);

var expectedTripledOutputValue = inputValue * 3;
var receivedTripledOutputValue = new TriplesYourNumber(inputValue).GetMultipliedValue();

Assert.AreEqual(expectedTripledOutputValue , receivedTripledOutputValue);
  • Tecnicamente, stiamo ancora scrivendo un test che verifica se il const in base(value, 2) corrisponde al const in inputValue * 2 .
  • Tuttavia, siamo allo stesso tempo anche testando che questa classe sia correttamente moltiplicando qualsiasi valore dato da questo fattore predeterminato .

Il primo punto elenco non è rilevante per il test. Il secondo è!

    
risposta data 17.04.2018 - 14:59
fonte

Leggi altre domande sui tag