Test più facili con la derisione

È una sfortunata verità che, sebbene il principio base alla base del test sia piuttosto semplice, l'introduzione completa di questo processo nel tuo flusso di lavoro di codifica quotidiano è più difficile di quanto tu possa sperare. Solo i vari gerghi possono essere travolgenti! Fortunatamente, una varietà di strumenti ti dà le spalle e aiuta a rendere il processo il più semplice possibile. Mockery, il principale framework di oggetti fittizi per PHP, è uno di questi strumenti!

In questo articolo approfondiremo cosa è il mocking, perché è utile e come integrare Mockery nel flusso di lavoro di test.


Derisione decodificata

Un oggetto fittizio non è altro che un po 'di gergo di prova che si riferisce alla simulazione del comportamento di oggetti reali. In termini più semplici, spesso, quando si esegue il test, non si desidera eseguire un particolare metodo. Invece, devi semplicemente assicurarti che sia stato, di fatto, chiamato.

Forse un esempio è in ordine. Immagina che il tuo codice attivi un metodo che registrerà un po 'di dati in un file. Quando si verifica questa logica, certamente non si desidera toccare fisicamente il file system. Questo ha il potenziale per ridurre drasticamente la velocità dei test. In queste situazioni, è meglio prendere in giro la classe del file system e, anziché leggere manualmente il file per provare che è stato aggiornato, è sufficiente assicurarsi che il metodo applicabile alla classe sia stato effettivamente chiamato. Questo è beffardo! Non c'è nient'altro di quello; simulare il comportamento degli oggetti.

Ricorda: il gergo è solo un gergo. Non consentire mai un pezzo di terminologia confusa per impedirti di apprendere una nuova abilità.

In particolare con la maturazione del tuo processo di sviluppo, compreso il principio di responsabilità individuale e sfruttando l'iniezione di dipendenza, una familiarità con la derisione diventerà rapidamente essenziale.

Mazze contro Stub: Le probabilità sono alte che sentirai spesso i termini, finto e mozzicone, gettato in modo intercambiabile. In realtà, i due servono a scopi diversi. Il primo si riferisce al processo di definizione delle aspettative e di garanzia del comportamento desiderato. In altre parole, una simulazione può potenzialmente portare a un test fallito. Un mozzicone, d'altra parte, è semplicemente una serie fittizia di dati che possono essere passati in giro per soddisfare determinati criteri.

La libreria di prova defacto per PHP, PHPUnit, viene fornita con la propria API per la simulazione di oggetti; tuttavia, sfortunatamente, può risultare complicato lavorare con. Come sicuramente saprai, più i test sono difficili, più è probabile che lo sviluppatore semplicemente (e purtroppo) non lo faccia.

Fortunatamente, una varietà di soluzioni di terze parti sono disponibili tramite Packagist (repository di pacchetti di Composer), che consente una maggiore leggibilità e, cosa più importante, scrivibilità. Tra queste soluzioni - e la più notevole del set - c'è Mockery, un framework di oggetti fittizi framework-agnostico.

Progettato come un'alternativa drop-in per coloro che sono sopraffatti dalla verbosità beffarda di PHPUnit, Mockery è un'utilità semplice ma potente. Come sicuramente troverai, infatti, è lo standard del settore per lo sviluppo moderno di PHP.


Installazione

Come la maggior parte dei moderni strumenti PHP, Mockery può essere installato con Composer.

Al giorno d'oggi, come la maggior parte degli strumenti PHP, il metodo consigliato per installare Mockery è tramite Composer (sebbene sia disponibile anche tramite Pear).

Aspetta, cos'è questa cosa del compositore? È lo strumento preferito dalla comunità PHP per la gestione delle dipendenze. Fornisce un modo semplice per dichiarare le dipendenze di un progetto e inserirle con un singolo comando. Come sviluppatore PHP moderno, è fondamentale avere una conoscenza di base di cosa sia il compositore e come usarlo.

Se si lavora lungo, a scopo di apprendimento, aggiungere un nuovo composer.json file su un progetto vuoto e aggiungi:

 "require-dev": "mockery / mockery": "dev-master"

Questo bit di JSON specifica che, per lo sviluppo, l'applicazione richiede la libreria di Mockery. Dalla riga di comando, a compositore installa --dev tirerà dentro il pacchetto.

$ compositore installa --dev Caricamento dei repository di compositore con informazioni sul pacchetto Installazione delle dipendenze (incluso require-dev) - Installazione di mockery / mockery (dev-master 5a71299) Clonazione 5a712994e1e3ee604b0d355d1af342172c6f475f Scrittura del file di blocco Generazione dei file di caricamento automatico

Come bonus aggiuntivo, Composer viene fornito gratuitamente con il proprio caricatore automatico! Specificare una classmap di directory e compositore dump-autoload, o seguire lo standard PSR-0 e regolare la struttura della directory in modo che corrisponda. Fare riferimento a Nettuts + per saperne di più. Se stai ancora richiedendo manualmente innumerevoli file in ogni file PHP, beh, potresti semplicemente sbagliarti.


Il dilemma

Prima di poter implementare una soluzione, è meglio prima esaminare il problema. Immagina di aver bisogno di implementare un sistema per gestire il processo di generazione del contenuto e scriverlo in un file. Forse il generatore compila vari dati, da stub di file locali o da un servizio Web, e quindi i dati vengono scritti nel file system.

Se segue il principio della responsabilità unica - che stabilisce che ogni classe dovrebbe essere responsabile di esattamente una cosa - quindi è ovvio che dovremmo suddividere questa logica in due classi: una per generare il contenuto necessario e un'altra per scrivere fisicamente i dati in un file. UN Generatore e File la classe, rispettivamente, dovrebbe fare il trucco.

Mancia: Perché non usare file_put_contents direttamente dal Generatore classe? Bene, chiediti: "Come potrei provare questo?"Esistono tecniche, come le patch per le scimmie, che possono permetterti di sovraccaricare questo tipo di cose, ma, come best practice, è meglio invece avvolgere tali funzionalità, in modo che possa essere facilmente derisa con strumenti come Mockery!

Ecco una struttura di base (con una buona dose di pseudo codice) per il nostro Generatore classe.

file = $ file;  funzione protetta getContent () // semplificata per la demo return 'foo bar';  public function fire () $ content = $ this-> getContent (); $ this-> file-> put ('foo.txt', $ content); 

Iniezione di dipendenza

Questo codice sfrutta ciò che chiamiamo iniezione di dipendenza. Ancora una volta, questo è semplicemente il gergo degli sviluppatori per iniettare le dipendenze di una classe attraverso il suo metodo di costruzione, piuttosto che codificarle.

Perché questo è benefico? Perché altrimenti non saremmo in grado di deridere il File classe! Certo, potremmo deridere il File classe, ma se la sua istanziazione è hard-coded nella classe che stiamo testando, non c'è un modo semplice per sostituire quell'istanza con la versione mocked.

funzione pubblica __construct () // anti-pattern $ this-> file = new File; 

Il modo migliore per creare un'applicazione verificabile consiste nell'accostare ogni nuova chiamata al metodo con la domanda "Come potrei provare questo?"Anche se ci sono trucchi per aggirare questo hard-coding, farlo è considerato una cattiva pratica, ma iniettare sempre le dipendenze di una classe attraverso il costruttore o tramite l'iniezione di setter.

L'iniezione del setter è più o meno identica all'iniezione del costruttore. Il principio è esattamente lo stesso; l'unica differenza è che, piuttosto iniettando le dipendenze della classe attraverso il suo metodo di costruzione, sono invece fatte attraverso un metodo setter, in questo modo:

funzione pubblica setFile (file $ file) $ this-> file = $ file; 

Una critica comune all'iniezione di dipendenza è che introduce una complessità aggiuntiva in un'applicazione, il tutto per renderlo più verificabile. Sebbene l'argomento della complessità sia discutibile nell'opinione di questo autore, se si preferisce, è possibile consentire l'iniezione delle dipendenze, mentre si specificano ancora i valori predefiniti di fallback. Ecco un esempio:

class Generator public function __construct (File $ file = null) $ this-> file = $ file?: nuovo File; 

Ora, se un'istanza di File viene passato al costruttore, quell'oggetto sarà usato nella classe. D'altra parte, se non viene passato nulla, il Generatore volontà ricaderci per istanziare manualmente la classe applicabile. Ciò consente variazioni come:

# Class istanzia File nuovo generatore; # Inject File new Generator (nuovo file); # Inietta un mock di file per testare il nuovo generatore ($ mockedFile);

Proseguendo, per gli scopi di questo tutorial, il File la classe non sarà altro che un semplice wrapper su PHP file_put_contents funzione.

 

Piuttosto semplice, eh? Scriviamo un test per vedere, di prima mano, qual è il problema.

fuoco(); 

Si noti che questi esempi presumono che le classi necessarie siano state caricate automaticamente con Composer. Il tuo composer.json il file accetta facoltativamente un autoload oggetto, dove è possibile specificare le directory o le classi da caricare automaticamente. Non più disordinato richiedere dichiarazioni!

Se lavori insieme, corri PHPUnit tornerà:

OK (1 test, 0 asserzioni)

È verde; questo significa che possiamo passare al prossimo compito, giusto? Bene, non esattamente. Mentre è vero che il codice funziona, anzi, funziona, ogni volta che viene eseguito questo test, a foo.txt il file verrà creato sul file system. Che dire quando hai scritto decine di altri test? Come puoi immaginare, molto rapidamente, la velocità di esecuzione del tuo test balbetterà.

Sebbene i test superino, toccano erroneamente il filesystem.

Non sei ancora convinto? Se la velocità ridotta del test non ti influenzerà, allora considera il buon senso. Pensaci: stiamo testando il Generatore classe; perché abbiamo qualche interesse nell'eseguire il codice dal File classe? Dovrebbe avere i suoi test! Perché diamine dovremmo raddoppiare?


La soluzione

Si spera che la sezione precedente abbia fornito l'illustrazione perfetta per spiegare perché il derisione è essenziale. Come è stato notato in precedenza, sebbene potessimo utilizzare l'API nativa di PHPUnit per soddisfare i nostri requisiti di derisione, non è eccessivamente piacevole lavorare con. Per illustrare questa verità, ecco un esempio per affermare che un oggetto deriso dovrebbe ricevere un metodo, getName e ritorno John Doe.

funzione pubblica testNativeMocks () $ mock = $ this-> getMock ('SomeClass'); $ mock-> attende ($ this-> once ()) -> method ('getName') -> will ($ this-> returnValue ('John Doe')); 

Mentre ottiene il lavoro fatto - affermando che a getName il metodo viene chiamato una volta e restituisce John Doe - L'implementazione di PHPUnit è confusa e prolissa. Con Mockery, possiamo migliorare drasticamente la sua leggibilità.

funzione pubblica testMockery () $ mock = Mockery :: mock ('SomeClass'); $ mock-> shouldReceive ('getName') -> once () -> andReturn ('John Doe'); 

Si noti come l'ultimo esempio legge (e parla) meglio.

Continuando con l'esempio del precedente "Dilemma sezione, questa volta, all'interno del GeneratorTest classe, invece prendiamo in giro - o simuliamo il comportamento di - il File classe con Mockery. Ecco il codice aggiornato:

shouldReceive ('put') -> con ('foo.txt', 'foo bar') -> once (); $ generator = new Generator ($ mockedFile); $ Generator-> fuoco (); 

Confuso dal Derisione :: close () riferimento all'interno del demolire metodo? Questa chiamata statica pulisce il contenitore di Mockery utilizzato dal test corrente ed esegue tutte le attività di verifica necessarie per le tue aspettative.

Una classe può essere derisa usando il leggibile Derisione :: finto () metodo. Successivamente, in genere dovrai specificare i metodi su questo oggetto fittizio che ti aspetti di chiamare, insieme a qualsiasi argomento applicabile. Questo può essere realizzato, tramite il shouldReceive (metodo) e con (ARG) metodi.

In questo caso, quando chiamiamo $ Generare-> fire (), stiamo affermando che dovrebbe chiamare il mettere metodo sul File istanza e invialo il percorso, foo.txt, e i dati, foo bar.

// libraries / Generator.php public function fire () $ content = $ this-> getContent (); $ this-> file-> put ('foo.txt', $ content); 

Poiché stiamo usando l'iniezione di dipendenza, ora è un gioco da ragazzi per iniettare il beffardo File oggetto.

$ generator = new Generator ($ mockedFile);

Se eseguiamo nuovamente i test, restituiranno comunque verde, tuttavia, il File la classe - e, di conseguenza, il file system - non sarà mai toccata! Ancora una volta, non c'è bisogno di toccare File. Dovrebbe avere i suoi test! Scherzando per la vittoria!

Semplici oggetti finti

Gli oggetti fittizi non devono sempre fare riferimento a una classe. Se si richiede solo un oggetto semplice, forse per un utente, è possibile passare un array al finto metodo - dove, per ogni articolo, la chiave e il valore corrispondono rispettivamente al nome del metodo e al valore restituito.

funzione pubblica testSimpleMocks () $ user = Mockery :: mock (['getFullName' => 'Jeffrey Way']); $ User-> getFullName (); // Jeffrey Way

Valori di ritorno dai metodi falsi

Ci saranno sicuramente dei momenti in cui un metodo di classe deriso deve restituire un valore. Continuando con il nostro esempio di generatore / file, cosa succede se dobbiamo assicurarci che, se il file esiste già, non dovrebbe essere sovrascritto? Come potremmo realizzarlo?

La chiave è usare il e ritorno() metodo sul tuo oggetto deriso per simulare diversi stati. Ecco un esempio aggiornato:

test di funzione pubblicaDoesNotOverwriteFile () $ mockedFile = Mockery :: mock ('File'); $ mockedFile-> shouldReceive ('exists') -> once () -> andReturn (true); $ mockedFile-> shouldReceive ('put') -> never (); $ generator = new Generator ($ mockedFile); $ Generator-> fuoco (); 

Questo codice aggiornato ora afferma che a esiste il metodo dovrebbe essere attivato sul beffardo File classe, e dovrebbe, per gli scopi del percorso di questo test, restituire vero, segnalando che il file esiste già e non dovrebbe essere sovrascritto. Successivamente assicuriamo che, in situazioni come questa, il mettere metodo sul File la classe non viene mai attivata. Con Mockery, questo è facile, grazie a mai() aspettativa.

$ mockedFile-> shouldReceive ('put') -> never ();

Se dovessimo eseguire nuovamente i test, verrà restituito un errore:

Il metodo esiste () da File deve essere chiamato esattamente 1 volte ma chiamato 0 volte.

Aha; quindi il test lo aspettava $ This-> file-> esiste () dovrebbe essere chiamato, ma non è mai successo. Come tale, ha fallito. Risolviamolo!

file = $ file;  funzione protetta getContent () // semplificata per la demo return 'foo bar';  public function fire () $ content = $ this-> getContent (); $ file = 'foo.txt'; se (! $ this-> file-> esiste ($ file)) $ this-> file-> put ($ file, $ contenuto); 

Questo è tutto ciò che c'è da fare! Non solo abbiamo seguito un ciclo TDD (test-driven development), ma i test sono tornati al verde!

È importante ricordare che questo tipo di test è efficace solo se collaudi anche le dipendenze della tua classe! In caso contrario, sebbene i test possano mostrare il verde, per la produzione, il codice si interromperà. La nostra dimostrazione finora lo ha solo garantito Generatore funziona come previsto. Non dimenticare di testare File anche!


aspettative

Analizziamo un po 'più a fondo le dichiarazioni di aspettativa di Mockery. Conoscete già shouldReceive. Stai attento con questo, però; il suo nome è un po 'fuorviante. Se lasciato da solo, non richiede che il metodo debba essere attivato; il valore predefinito è zero o più volte (zeroOrMoreTimes ()). Per affermare che è necessario chiamare il metodo una volta o potenzialmente più volte, sono disponibili una serie di opzioni:

$ Mock> shouldReceive ( 'metodo') -> una volta (); $ Mock> shouldReceive ( 'metodo') -> volte (1); $ Mock> shouldReceive ( 'metodo') -> atleast () -> volte (1);

Ci saranno momenti in cui sono necessari ulteriori vincoli. Come dimostrato in precedenza, questo può essere particolarmente utile quando è necessario assicurarsi che un determinato metodo venga attivato con gli argomenti necessari. È importante tenere presente che l'aspettativa si applica solo se un metodo viene chiamato con questi argomenti esatti.

Ecco alcuni esempi.

$ Mock> shouldReceive ( 'get') -> withAnyArgs () -> una volta (); // default $ mock-> shouldReceive ('get') -> con ('foo.txt') -> once (); $ mock-> shouldReceive ('put') -> con ('foo.txt', 'foo bar') -> once ();

Questo può essere esteso ulteriormente per consentire che i valori degli argomenti siano di natura dinamica, purché soddisfino determinati criteri. Forse vogliamo solo assicurarci che una stringa venga passata a un metodo:

$ Mock> shouldReceive ( 'get') -> con (Mockery tipo :: ( 'string')) -> una volta ();

O forse l'argomento deve corrispondere a un'espressione regolare. Diciamo che qualsiasi nome di file che finisce con .testo dovrebbe essere abbinato.

$ mockedFile-> shouldReceive ('put') -> con ('/ \. txt $ /', Mockery :: any ()) -> once ();

E come esempio finale (ma non limitato a), consentiamo una matrice di valori accettabili, usando il qualsiasi Matcher.

$ mockedFile-> shouldReceive ('get') -> with (Mockery :: anyOf ('log.txt', 'cache.txt')) -> once ();

Con questo codice, l'aspettativa si applica solo se il primo argomento al ottenere il metodo è log.txt o cache.txt. Altrimenti, verrà lanciata un'eccezione Mockery durante l'esecuzione dei test.

Mockery \ Exception \ NoMatchingExpectationException: nessun gestore corrispondente trovato ... 

Mancia: Non dimenticare, puoi sempre alias Presa in giro come m ai vertici della tua classe per rendere le cose un po 'più concise: usare Mockery come m;. Ciò consente il più sintetico, m :: finto ().

Infine, abbiamo una varietà di opzioni per specificare ciò che il metodo beffardo dovrebbe fare o restituire. Forse abbiamo solo bisogno di restituire un booleano. Facile:

$ mock-> shouldReceive ('method') -> once () -> andReturn (false);

Misteri parziali

Potresti scoprire che ci sono situazioni in cui devi solo prendere in giro un singolo metodo, piuttosto che l'intero oggetto. Immaginiamo, ai fini di questo esempio, che un metodo sulla classe faccia riferimento a una funzione globale personalizzata (gasp) per recuperare un valore da un file di configurazione.

getOption ( 'timeout'); // fare qualcosa con $ timeout

Mentre ci sono alcune tecniche differenti per il derisione delle funzioni globali. tuttavia, è meglio evitare questo metodo per chiamare tutti insieme. Questo è precisamente quando entrano in gioco le parate parziali.

funzione pubblica testPartialMockExample () $ mock = Mockery :: mock ('MyClass [getOption]'); $ mock-> shouldReceive ('getOption') -> once () -> andReturn (10000); $ Mock> fuoco (); 

Nota come abbiamo messo il metodo per fingere tra parentesi. Se hai più metodi, separali semplicemente con una virgola, in questo modo:

$ mock = Mockery :: mock ('MyClass [metodo1, metodo2]');

Con questa tecnica, il resto dei metodi sull'oggetto si innescherà e si comporterà come farebbe normalmente. Tieni presente che devi sempre dichiarare il comportamento dei tuoi metodi di simulazione, come abbiamo fatto sopra. In questo caso, quando getOption viene chiamato, piuttosto che eseguire il codice al suo interno, semplicemente restituiamo 10000.

Un'opzione alternativa consiste nell'utilizzare passamontagna parziale passiva, che si può pensare come impostazione di uno stato predefinito per l'oggetto mock: tutti i metodi rimandano alla classe genitore principale, a meno che non sia specificata un'aspettativa.

Lo snippet di codice precedente potrebbe essere riscritto come:

funzione pubblica testPassiveMockExample () $ mock = Mockery :: mock ('MyClass') -> makePartial (); $ mock-> shouldReceive ('getOption') -> once () -> andReturn (10000); $ Mock> fuoco (); 

In questo esempio, tutti i metodi su La mia classe si comporteranno come normalmente farebbero, escludendo getOption, che verrà deriso e restituito 10000 '.


Hamcrest

La biblioteca di Hamcrest fornisce un set aggiuntivo di giocatori per definire le aspettative.

Una volta che hai familiarizzato con l'API di Mockery, ti consigliamo di utilizzare anche la libreria di Hamcrest, che fornisce un set aggiuntivo di lettori per definire aspettative leggibili. Come Mockery, può essere installato tramite Composer.

"require-dev": "mockery / mockery": "dev-master", "davedevelopment / hamcrest-php": "dev-master"

Una volta installato, puoi usare una notazione più leggibile per definire i tuoi test. Di seguito sono riportati alcuni esempi, tra cui piccole variazioni che raggiungono lo stesso risultato finale.

 

Osserva come Hamcrest ti permette di scrivere le tue asserzioni in modo leggibile o conciso come desideri. L'uso del è() la funzione non è altro che zucchero sintattico per facilitare la leggibilità.

Scoprirai che Mockery si fonde abbastanza bene con Hamcrest. Ad esempio, con Mockery da solo, per specificare che un metodo di simulazione dovrebbe essere chiamato con un singolo argomento di tipo, stringa, potresti scrivere:

$ mock-> shouldReceive ('metodo') -> con (Mockery :: type ('string')) -> once ();

Se usi Hamcrest, Mockery :: tipo può essere sostituito con valore stringa(), così:

$ mock-> shouldReceive ('metodo') -> con (stringValue ()) -> once ();

Hamcrest segue il risorsaConvenzione di denominazione dei valori per la corrispondenza del tipo di valore.

  • NullValue
  • valore intero
  • arrayValue
  • risciacqua e ripeti

In alternativa, per abbinare qualsiasi argomento, Derisione :: qualsiasi () può diventare nulla().

$ file-> shouldReceive ('put') -> con ('foo.txt', anything ()) -> once ();

Sommario

Il più grande ostacolo all'uso di Mockery è, ironicamente, non l'API stessa.

Il più grande ostacolo all'uso di Mockery è, ironicamente, non l'API stessa, ma capire perché e quando usare i mock nei test.

La chiave è imparare e rispettare il principio di responsabilità unica nel flusso di lavoro di codifica. Coniato da Bob Martin, l'SRP dice che una classe "dovrebbe avere una, e solo una, ragione per cambiare."In altre parole, non è necessario aggiornare una classe in risposta a più modifiche non correlate all'applicazione, come la modifica della logica di business o la modalità di formattazione dell'output o la modalità di persistenza dei dati. come un metodo, una classe dovrebbe fare una cosa.

Il File class gestisce le interazioni tra file system. UN MySQLdb repository persiste i dati. Un E-mail classe prepara e invia e-mail. Notate come, in nessuno di questi esempi era la parola, e, Usato.

Una volta compreso, il test diventa notevolmente più semplice. L'iniezione di dipendenza deve essere utilizzata per tutte le operazioni che non rientrano nella classe ombrello. Durante i test, concentrati su una classe alla volta e prendi in giro tutte le sue dipendenze. Non sei interessato a testarli comunque; hanno i loro test!

Sebbene nulla ti impedisca di utilizzare l'implementazione di mocking nativa di PHPUnit, perché preoccuparti quando la leggibilità migliorata di Mockery è solo un aggiornamento del compositore lontano?