È buona abitudine eseguire lo stub di un oggetto non perché sia lento, ma per evitare di testarlo due volte?

4

Diciamo, per esempio, che voglio testare che un avviso è mostrato sul cruscotto di un'auto solo quando il motore è rotto.

Il metodo che implementa questa funzionalità potrebbe avere il seguente aspetto:

class Dashboard {
    function showWarningIfEngineIsBroken() {
        if ($this->engine->isBroken()) {
            $this->showWarning = true;
        }
}

Diciamo che ci sono 10 scenari in cui $this->engine->isBroken() restituirebbe true . Durante il test del dashboard, vorrei assicurarmi che la spia si accenda in uno di questi scenari. Quindi una cosa che potrei fare è creare una classe di test come questa:

class DashboardTest {
    function showsWarningIfEngineIsBrokenBecauseOfReasonA() { ... }
    function showsWarningIfEngineIsBrokenBecauseOfReasonB() { ... }
    function showsWarningIfEngineIsBrokenBecauseOfReasonC() { ... }
    // etc.
}

Questo non è un problema, fino a quando voglio anche testare che le porte si sbloccano automaticamente non appena il motore si rompe. Poiché ciò creerebbe molti test "duplicati":

class DoorTest {
    function unlocksDoorIfEngineIsBrokenBecauseOfReasonA() { ... }
    function unlocksDoorIfEngineIsBrokenBecauseOfReasonB() { ... }
    function unlocksDoorIfEngineIsBrokenBecauseOfReasonC() { ... }
    // etc.
}

Una soluzione sarebbe testare Engine::isBroken() invece, e nel test per Dashboard e Door, verifica solo cosa succede se isBroken restituisce true o false :

class EngineTest {
    function isBrokenReturnsTrueIfEngineIsBrokenBecauseOfReasonA() { ... }
    function isBrokenReturnsTrueIfEngineIsBrokenBecauseOfReasonB() { ... }
    function isBrokenReturnsTrueIfEngineIsBrokenBecauseOfReasonC() { ... }
    // etc.
}

class DashboardTest {
    function showsWarningIfEngineIsBroken() {
        $stubEngine = ...; // some stub of engine that returns true for 'isBroken'
        $dashboard = new Dashboard($stubEngine);
        $dashboard->showWarningIfEngineIsBroken();
        $this->assertTrue($dashboard->showWarning);
    }
}

class DoorTest {
    function unlocksIfEngineIsBroken() {
        $stubEngine = ...; // some stub of engine that returns true for 'isBroken'
        $door = new Door($stubEngine);
        $door->unlockIfEngineIsBroken();
        $this->assertTrue($door->isUnlocked);
    }
}

Quindi alla fine la domanda: a quanto ho capito, i doppi di prova dovrebbero essere usati per evitare che il test dell'unità rallenti o per assicurarsi che rimanga isolato. In questo caso, supponiamo che $engine->isBroken() sia effettivamente molto veloce e non abbia effetti collaterali. È una buona pratica fermarlo ancora, poiché in realtà voglio solo testare cosa farebbero Door o Dashboard quando restituisce true o false ?

    
posta user125661 29.06.2017 - 05:09
fonte

2 risposte

6

Diciamo che hai una bella serie di test. Come hai codificato è diventato un po 'grande. Comunque bello e veloce però. Quindi stai facendo un sacco di test ogni volta. Ora hai deciso di cambiare il funzionamento del motore. Tu fai e alcuni test del motore falliscono. va bene. Ma ora alcuni test di Dashboard stanno fallendo. Il che è strano perché non pensavi di aver cambiato la Dashboard. Forse l'hai fatto per caso. E ora stai cercando un bug nel posto sbagliato.

La velocità non è solo un motivo per usare il doppio. L'isolamento è importante. Non per il bene che passa il test quando dovrebbe, ma per evitare che fallisca quando non dovrebbe.

    
risposta data 29.06.2017 - 05:52
fonte
0

Se scrivi dei test unitari, vuoi sapere dove non viene soddisfatta un'asserzione. Quindi i tuoi test unitari dovrebbero testare solo le piccole unità del tuo sistema per essere espressive. Un test unitario non dovrebbe fallire a causa di qualcosa per cui il test non è stato scritto.

Quando testate due artefatti di codice e un artefatto di codice dipendente viene usato in entrambi, allora due test potrebbero fallire per un motivo per cui i test non sono stati progettati se non si stubano / deridono il manufatto da cui dipendono entrambi.

Se non deformi artefatti dipendenti, un test fallirà perché l'artefatto in esame potrebbe non funzionare OPPURE l'artefatto dipendente. Se testate anche il manufatto dipendente, avete almeno due test che potrebbero fallire. Perderai espressività.

In sistemi enormi vedo spesso che dieci o più test falliscono per lo stesso motivo. Questo ha qualcosa da fare che i test non sono isolati.

Un altro punto è derisione parziale. La maggior parte degli sviluppatori non ha opinioni o rifiuta questo tipo di test. L'argomento è: se devi prendere in giro parzialmente, hai violato l'SRP (principio di responsabilità singola) prima. Mentre questo è totalmente vero, non ha assolutamente NULLA di fare con il mondo reale.

Nel mondo reale abbiamo a che fare con il codice imperfetto anche se è un codice ben scritto che viola la SRP il più delle volte. Questo non è poi così male come si può migliorare il codice in qualsiasi momento. Ma corretto come l'argomento è: Nessuno è in grado di scrivere un codice perfetto e certamente non se il codice per un problema viene scritto la prima volta. Quindi SRP è molto importante ma nel mondo reale troverai solo pochi frammenti di codice che saranno completamente SRP.

Un altro punto è che potresti avere un oggetto complesso che ha metodi che si comportano in diversi modi dipendenti dallo stato attuale dell'oggetto. Per testare questo devi portare l'oggetto un ulteriore passo avanti nello stato ogni volta che vuoi testare il passo successivo.

Quindi hai la seguente struttura:

test 1

esegui il metodo 1 (sotto test)

asserisci risultato

test 2

esegui il metodo 1

esegui il metodo 2 (sotto test)

asserisci risultato

test 3

esegui il metodo 1

esegui il metodo 2

esegui il metodo 3 (sotto test)

asserisci risultato

Anche qui: tutti i test tranne il test 1 possono fallire a causa di altri motivi che il metodo in prova potrebbe fallire.

Quindi per me il parsing parziale è un modo per ottenere test isolati ed espressivi che falliranno solo a causa delle ragioni per cui sono stati scritti. Per l'ultimo esempio ti verrà proposta la seguente struttura:

test 1

esegui il metodo 1 (sotto test)

asserisci risultato

test 2

esegui il metodo 1 (mocked)

esegui il metodo 2 (sotto test)

asserisci risultato

test 3

esegui il metodo 2 (mocked)

esegui il metodo 3 (sotto test)

asserisci risultato

test 4

esegui il metodo 3 (mocked)

esegui il metodo 4 (sotto test)

asserisci risultato

Si vede che l'unico passo precedente viene deriso. Quindi il tuo test si concentrerà sul metodo sotto test e il suo risultato.

Quindi sì: dovresti prendere in giro / smettere manufatti dipendenti se la tua serie di casi di test dovrebbe essere isolata ed espressiva.

    
risposta data 29.06.2017 - 09:14
fonte

Leggi altre domande sui tag