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.