Test unitario: metodo per test rispetto a fornitore di dati

5

Sto discutendo un po 'di un argomento filosofico con uno dei miei colleghi riguardo al modo "giusto" di fare test unitari (in questo caso con PHPUnit). Sono dell'opinione che dovresti scrivere un metodo di prova nel test unitario per test che vuoi eseguire.

// Obviously a very contrived example!
class AdderTest extends TestCase
{
    private $object = NULL;

    protected function setup ()
    {
        $this -> object = new Adder ();
    }

    /**
     * Test that 2 + 2 = 4
     */
    public function testAddTwoTwoFour ()
    {
        $this -> assertEquals ($this -> object -> add (2, 2), 4);
    }

    /**
     * Test that 2 + 3 = 5
     */
    public function testAddTwoThreeFive ()
    {
        $this -> assertEquals ($this -> object -> add (2, 3), 5);
    }

    // and a bunch of other test cases
}

Il mio collega, tuttavia, pensa che sia meglio usare i fornitori di dati.

class AdderTest extends TestCase
{
    private $object = NULL;

    protected function setup ()
    {
        $this -> object = new Adder ();
    }

    /**
     * Test add method
     *
     * @dataProvider addTestDataSource
     */
    public function testAdd ($expected, $a, $b)
    {
        $this -> assertEquals ($this -> object -> add ($a, $b), $expected);
    }

    private function addTestDataSource ()
    {
        return [
            [4, 2, 2], // Equivalent to testAddTwoTwoFour
            [5, 2, 3], // Equivalent to testAddTwoThreeFive
        ];
    }
}

La mia opinione personale è che il primo è migliore perché:

  • Un metodo di test = 1 test, e ogni test unitario dovrebbe verificare uno e un solo fatto sull'unità in prova è corretto.
  • I fallimenti sono facili da individuare perché il nome del metodo di prova fallito viene visualizzato
  • Sono dell'opinione che i test unitari non debbano in nessun caso tentare di essere affatto "intelligenti" sulla base del fatto che un errore di test significa che il codice sotto test è sbagliato o il test stesso è sbagliato *, e più intelligente è il test, più è difficile escludere il test come fonte di errore. Dovresti eseguire il debug del codice, non i suoi test unitari.
  • Nel caso di PHPUnit si basa su "magic" per funzionare (vale a dire PHPUnit che decodifica il tag @dataProvider)
  • Se vuoi fare diversi tipi di asserzioni devi comunque scrivere altri metodi di test.

Il mio collega preferisce il suo metodo perché:

  • Un test di unità per caso viola Non ripeti te stesso
  • Un test di unità per caso incoraggia la copia e incolla, che è normalmente considerata una cattiva pratica
  • Se l'interfaccia dell'unità in prova cambia, devi cambiare molti metodi di test (che possono essere soggetti a errori) mentre devi solo cambiare un metodo di test e un metodo di fornitura dati nel caso del provider di dati
  • Il test unitario è molto più conciso e quindi più facile da capire.

Mentre io (naturalmente) penso di essere corretto, il mio collega solleva alcuni punti validi. Pensi che il mio approccio sia migliore o preferiresti il suo? Qual è il tuo ragionamento per la tua scelta, soprattutto se tale ragionamento non è negli elenchi che ho fornito sopra?

* presumendo che non ci siano bug nel framework dei test unitari, naturalmente!

    
posta GordonM 25.01.2017 - 16:16
fonte

2 risposte

10

Dipende.

Dovresti chiederti: quale proprietà del codice è questo particolare test? Che cosa so quando so che questo test è verde?

Non intendo questo come un esercizio filosofico, ma in un senso molto pratico. Prendi il tuo Adder, per esempio. Puoi chiedere "Puoi aggiungere 2 e 2 correttamente?" e "Puoi aggiungere 2 e 3 correttamente?" Quindi dovresti scrivere questi test:

public function knowsTwoPlusTwoIsFour ()
public function knowsTwoPlusThreeIsFive ()

Ma forse non vuoi saperlo. Forse quello che vuoi veramente sapere è "Puoi aggiungere correttamente due numeri di ordinario?" (Spiegherò cosa significa "ordinario" più avanti) Ora il test dovrebbe utilizzare un fornitore di dati. Il test dovrebbe essere

public function canAddOrdinaryNumbers()

e la tua fonte di dati dovrebbe includere i soliti casi "normali", vale a dire aggiungere zero, aggiungere numeri negativi, aggiungerne il complemento:

[4, 2, 2],    // Duh
[-1, 2, -3],  // negative summand
[0, 0, 0],    // just to make sure
[2, 2, 0],    // neutral element
[0, 2, -2],   // inverse

Ora il tuo test ti dice che il sommatore non ha problemi con i numeri normali. Questo è fondamentalmente un pezzo di informazione, quindi dovrebbe essere un metodo di prova.

E i numeri che non sono comuni?

public function canAddOneToNaN ()
public function knowsWhatInfPlusNegInfIs ()
public function croaksOnFLoatingPointInput ()
public function whatIsOnePlusSqrtOfMinusOneAnyway ()

Lascio l'implementazione di questi casi di test come esercizio al lettore e vorrei concentrarmi su questo punto: tutti i metodi di test unitari in una classe dovrebbero trasmettere la stessa quantità di informazioni. Questa è una misura soggettiva, ovviamente, ma penso che sia una buona regola: guarda i test e chiediti: alcuni di questi test mi dicono solo roba che altri già facevano prima? Alcuni test sembrano avere più significato di altri? Se è così, allora potresti voler fare un refactoring.

    
risposta data 25.01.2017 - 17:41
fonte
4

Molti dei tuoi punti derivano da punti deboli del tuo framework di test. Se guardiamo Spock , risolve un certo numero di dubbi. Vediamo come scrivere il test in Spock

@Unroll
final "add #a and #b gives #expected"() {
    expect:
        add(a, b) == expected
    where:
        a  | b  || expected
        2  | 2  || 4
        2  | 3  || 5
        // etc.
}

One test method = 1 test, and every unit test should verify one and only one fact about the unit under test is correct.

"Un solo e unico fatto" può essere un concetto nebuloso. I test basati sui dati, come i test basati sulle proprietà, possono essere utilizzati per ottenere dati di livello superiore che nessun singolo caso di test può realmente mostrare: ad es. add è commutativo / associativo / ha un'identità a 0 .

Failures are easy to spot because the name of the failed test method is displayed

Spock ti consente di fare in modo che il nome del test fallito dipenda dal valore. Anche senza questa funzione, un messaggio di asserzione farà capire quali valori hanno fallito.

I'm of the opinion that unit tests should under no circumstances attempt to be at all "clever"

Un test guidato dai dati non deve essere più intelligente di un ciclo for-each. In effetti, senza il supporto del framework di testing è possibile implementare test basati sui dati con un semplice loop. E puoi sempre ricorrere a un ciclo for-each se la "magia" del framework è troppo intelligente. Tuttavia, quella magia come in Spock può dare test altamente leggibili.

If you want to do different types of assertions you have to write additional test methods anyway.

Sì, ma non vedo questo come un problema. Come dici tu, devi scrivere metodi aggiuntivi a prescindere e il bello dei test basati sui dati è che puoi estrarre i tuoi dati e alimentarli in più metodi.

In generale, ritengo che i tuoi colleghi sul punto "DRY / eliminando il copia-incolla" e la leggibilità siano le grandi vittorie dei test basati sui dati. Un'altra grande vittoria per i test basati sui dati è che rende l'aggiunta di un caso di test molto economico.

    
risposta data 25.01.2017 - 18:29
fonte

Leggi altre domande sui tag