Come passare un oggetto mock in una classe per i test unitari

1

Sembra che ci siano molti modi per passare un oggetto deriso in una classe per il test delle unità e non sono sicuro quale sia l'approccio corretto da adottare per la mia applicazione PHP.

Se stavo usando Dependency Injection allora potrei avere le dipendenze come argomenti del costruttore che potrei usare per passare i mock dei miei test:

class Example
{
    private $dependency;

    public function __construct(Dependency $dependency)
    {
        $this->dependency = $dependency;
    }
}

Tuttavia, non sto utilizzando Iniezione delle dipendenze, quindi non posso usare questo approccio.

Invece, potrei aggiungere un metodo pubblico per impostare la proprietà privata chiamata dal costruttore:

class Example
{
    private $dependency;

    public function __construct()
    {
        $this->setDependency(new Dependency());
    }

    public function setDependency(Dependency $dependency)
    {
        $this->dependency = $dependency;
    }
}

Ora i miei test possono utilizzare questo metodo pubblico per impostare la dipendenza sul mio valore fittizio.
Sembra l'approccio migliore , tuttavia ho aggiunto questo nuovo metodo setDependency solo per rendere il test più facile e che sia un po 'sporco.

In alternativa, potrei usare la reflection per impostare direttamente il valore di una proprietà protected , ma ora il mio test richiede conoscenza dell'implementazione della classe Example :

class Example
{
    protected $dependency;
}

class ExampleTest
{
    public function testDependency()
    {
        $example = new Example();

        $dependencyMock = $this->getMock(); // Truncated

        $this->setPrivateProperty($example, "dependency", $dependencyMock);
    }
}

Qual è il modo corretto in cui i miei test passano un oggetto deriso nella mia classe Example quando non utilizzo Dependency Injection?

    
posta nevada_scout 17.05.2018 - 13:50
fonte

1 risposta

4

However, I'm not using Dependency Injection so I cannot use this approach.

Sei sicuro di non esserlo? Perché quando scrivi un codice come questo:

class Example
{
    private $dependency;

    public function __construct(Dependency $dependency)
    {
        $this->dependency = $dependency;
    }
}

Che urla iniezione di dipendenza su di me (il tuo primo Example sopra è pronto per iniezione del costruttore ). Dopo tutto l'iniezione di dipendenza non è altro che un termine di fantasia per passare un parametro per diventare parte di stato . Ad alcuni piace usare strutture o librerie DI contenitore per fare il loro DI in ciò che equivale a una nuova lingua, ma altri come segui gli strumenti integrati nella loro lingua e utilizza Pure DI . Quello che hai fornito sopra è un esempio di puro DI.

In entrambi i casi, il punto principale è separare il codice di costruzione dal codice di comportamento. Finché lo fai ottieni il vantaggio principale di DI: la possibilità di riconfigurare senza riscrivere molto.

Un semplice schema per l'uso di Pure DI è quello di eseguire la composizione (costruzione) in main (o fino allo stack di chiamate che puoi ottenere). Ma in PHP main è solo la prima riga di codice . Quindi supponi che è dove le seguenti vite:

$example = new Example( new Dependency() );

C'è la tua iniezione. Nessun contenitore DI necessario. Se hai più iniezioni per più oggetti, mettili anche qui. In questo modo puoi creare un grafico ad oggetti collegato. Se è necessario modificare Dipendenza in modo che dipenda da qualcos'altro lo si costruisce anche qui. Spetta a te decidere se assegnare loro i nomi di $ . Qualunque cosa sia più leggibile. Solo non impazzire 1 . Ricorda, i nomi sono una buona cosa.

Una volta che tutto è stato composto / costruito, scrivi una riga e solo una riga di codice di comportamento per avviare il ticchettio del tuo oggetto grafico. Qualcosa come:

$example.start();

Questa è la fine del tuo codice procedurale. Tutto il resto è orientato agli oggetti.

Hai anche offerto:

class Example
{
    private $dependency;

    public function __construct()
    {
        $this->setDependency(new Dependency());
    }

    public function setDependency(Dependency $dependency)
    {
        $this->dependency = $dependency;
    }
}

My tests can now use this public method to set the dependency to my mocked value. This seems like the best approach, however I've added this new setDependency method just to make testing easier and that feels a bit dirty.

Ciò che rende questo sporco è che costringe la classe ad essere mutabile (lo stato può cambiare dopo la costruzione) solo per il gusto di test quando il test non ne ha bisogno. I test sono stati perfettamente soddisfatti del primo esempio.

Ma tu hai qualcosa qui che non avevi prima. Quello che hai qui è un valore di default noto. È un dolore reale dover configurare tutto quando ci sono evidenti impostazioni predefinite. Convention over configuration sostiene che è meglio progettare un sistema con una convenzione di valori predefiniti noti che può essere sovrascritta aggiungendo la configurazione codice, non semplicemente cambiando il codice di configurazione.

C'è un modo per dare test, convenzioni e configurazioni a tutto ciò di cui hanno bisogno, tutto senza forzare l'oggetto a essere mutabile:

class Example
{
    private $dependency;

    public function __construct( Dependency $dependency = new Dependency() )
    {
        $this->dependency = $dependency;
    }
}

Questo fa uso di argomenti facoltativi 2 . Ora quando vuoi la convenzione puoi usare:

$example = new Example();

ed è uguale a se hai usato

$example = new Example( new Dependency() );

ma sei libero di scrivere

$example = new Example( new DependencyMock() );

nei tuoi test o usalo nel tuo codice per configurare la via dalla convenzione.

Sì, tecnicamente la convenzione è hardcoded. Ma il valore hard coded è un valore predefinito sovrascrivibile. Questo fa un mondo di differenza. Se hai bisogno di usare i metodi di fabbrica, questo stesso trucco funziona anche con loro.

Infine, ci colpisci con questo:

class Example
{
    protected $dependency;
}

E poi inizi a parlare dell'uso del riflesso nei tuoi test.

What is the right way for my tests to pass a mocked object into my Example class when I'm not using Dependency Injection?

Il modo giusto è di riscrivere per accettare iniezioni. Come mostrato sopra che non richiede strumenti di fantasia. Puoi usare la riflessione per ignorare le protezioni di incapsulamento, ma in genere è meglio parametrizzare e lasciare che le iniezioni arrivino nella porta principale piuttosto che fare loro rompono le finestre nel buio della notte.

    
risposta data 17.05.2018 - 17:07
fonte

Leggi altre domande sui tag