Tutto su Mocking con PHPUnit

Esistono due stili di test: stili "scatola nera" e "scatola bianca". Il test della scatola nera si concentra sullo stato dell'oggetto; mentre il test della white box si concentra sul comportamento. I due stili si completano a vicenda e possono essere combinati per testare accuratamente il codice. beffardo ci permette di testare il comportamento, e questo tutorial combina il concetto di derisione con TDD per costruire una classe di esempio che utilizza diversi altri componenti per raggiungere il suo obiettivo.


Passaggio 1: Introduzione al test di comportamento

Gli oggetti sono entità che inviano messaggi a vicenda. Ogni oggetto riconosce una serie di messaggi a cui risponde a sua volta. Questi sono pubblico metodi su un oggetto. Privato i metodi sono l'esatto opposto. Sono completamente interni a un oggetto e non possono comunicare con qualcosa al di fuori dell'oggetto. Se i metodi pubblici sono simili ai messaggi, i metodi privati ​​sono simili ai pensieri.

Il totale di tutti i metodi, pubblici e privati, accessibili tramite metodi pubblici rappresenta il comportamento di un oggetto. Ad esempio, raccontando un oggetto a mossa fa sì che l'oggetto non solo interagisca con i suoi metodi interni, ma anche con altri oggetti. Dal punto di vista dell'utente, l'oggetto ha un solo semplice comportamento: esso si muove.

Dal punto di vista del programmatore, tuttavia, l'oggetto deve fare un sacco di piccole cose per ottenere il movimento.

Ad esempio, immagina che il nostro oggetto sia un'auto. Per farlo mossa, deve avere un motore acceso, essere nella prima marcia (o retromarcia) e le ruote devono girare. Questo è un comportamento che dobbiamo testare e sviluppare per progettare e scrivere il nostro codice di produzione.


Passo 2: Macchina giocattolo telecomandata

La nostra classe testata non utilizza mai veramente questi oggetti fittizi.

Immaginiamo di costruire un programma per il controllo remoto di una macchinina. Tutti i comandi alla nostra classe provengono dal telecomando. Dobbiamo creare una classe capisce ciò che il telecomando invia e genera comandi alla macchina.

Questa sarà un'applicazione di esercizio e supponiamo che le altre classi che controllano le varie parti dell'auto siano già state scritte. Conosciamo la firma esatta di tutte queste classi, ma sfortunatamente la casa automobilistica non ha potuto inviarci un prototipo, nemmeno il codice sorgente. Tutto quello che sappiamo sono i nomi delle classi, i metodi che hanno e il comportamento che ogni metodo incapsula. I valori di ritorno sono anche specificati.


Passaggio 3: schema dell'applicazione

Ecco lo schema completo dell'applicazione. Non ci sono spiegazioni a questo punto; è sufficiente tenerlo a mente per riferimento futuro.


Passaggio 4: test doppio

Un test Stub è un oggetto per controllare l'input indiretto del codice testato.

Il mocking è uno stile di test che richiede il proprio set di strumenti, una serie di oggetti speciali che rappresentano diversi livelli di falsificazione del comportamento dell'oggetto. Questi sono:

  • oggetti fittizi
  • matrici di prova
  • prova spie
  • banali test
  • prova i falsi

Ognuno di questi oggetti ha il loro scopo e comportamento speciali. In PHPUnit, sono creati con il $ This-> getMock () metodo. La differenza è come e per quali ragioni vengono utilizzati gli oggetti.

Per capire meglio questi oggetti, implementerò il "Controller per auto giocattolo" passo dopo passo usando i tipi di oggetti, in ordine, come elencato sopra. Ogni oggetto nella lista è più complesso dell'oggetto prima di esso. Ciò porta a un'implementazione radicalmente diversa rispetto al mondo reale. Inoltre, essendo un'applicazione immaginaria, userò alcuni scenari che potrebbero non essere nemmeno fattibili in una vera macchinina. Ma hey, immaginiamo cosa abbiamo bisogno di loro per capire il quadro generale.


Passaggio 5: Oggetto fittizio

Gli oggetti fittizi sono oggetti a cui il Sistema sotto test (SUT) dipende, ma in realtà non vengono mai utilizzati. Un oggetto fittizio può essere un argomento passato a un altro oggetto oppure può essere restituito da un secondo oggetto e quindi passato a un terzo oggetto. Il punto è che la nostra classe testata non usa mai questi oggetti fittizi. Allo stesso tempo, l'oggetto deve assomigliare ad un oggetto reale; altrimenti, il destinatario può rifiutarlo.

Il miglior modo di esemplificare questo è immaginare uno scenario; lo schema di cui, è qui sotto:

L'oggetto arancione è il RemoteControlTranslator. Lo scopo principale è quello di ricevere segnali dal telecomando e tradurli in messaggi per le nostre classi. Ad un certo punto, l'utente farà un "Pronto ad andare" azione sul telecomando. Il traduttore riceverà il messaggio e creerà le classi necessarie per rendere l'auto pronta.

Il produttore ha detto questo "Pronto ad andare" significa che il motore è avviato, il cambio è in folle e le luci sono accese o spente come da richiesta dell'utente.

Ciò significa che l'utente può predefinire lo stato delle luci prima di essere pronto ad andare, e si accenderanno o spegneranno in base a questo valore predefinito all'attivazione. RemoteControlTranslator quindi invia tutte le informazioni necessarie al CarControl classe' getReadyToGo ($ engine, $ gearbox, $ electronics, $ lights) metodo. So che questo è lontano da un design perfetto e viola alcuni principi e modelli, ma è molto buono per questo esempio.

Inizia il nostro progetto con questa struttura di file iniziale:

Ricorda, tutte le classi in CarInterface la cartella è fornita dal produttore dell'auto; non sappiamo la loro attuazione. Tutto ciò che sappiamo sono le firme di classe, ma a questo punto non ci importa di loro.

Il nostro obiettivo principale è implementare il CarController classe. Per testare questa classe, dobbiamo immaginare come vogliamo usarla. In altre parole, ci mettiamo nei panni del RemoteControlTranslator e / o qualsiasi altra futura classe che possa essere utilizzata CarController. Iniziamo creando il caso per la nostra classe.

classe CarControllerTest estende PHPUnit_Framework_TestCase 

Quindi aggiungere un metodo di prova.

 function testItCanGetReadyTheCar () 

Ora pensa a cosa dobbiamo passare al getReadyToGo () metodo: un motore, un cambio, un controller di elettronica e informazioni chiare. Per il bene di questo esempio, ci limiteremo a prendere in giro le luci:

require_once '... /CarController.php'; include '... /autoloadCarInterfaces.php'; class CarControllerTest estende PHPUnit_Framework_TestCase function testItCanGetReadyTheCar () $ carController = new CarController (); $ engine = new Engine (); $ cambio = nuovo cambio (); $ electornics = new Electronics (); $ dummyLights = $ this-> getMock ('Lights'); $ this-> assertTrue ($ carController-> getReadyToGo ($ engine, $ gearbox, $ electornics, $ dummyLights)); 

Questo ovviamente fallirà con:

PHP Errore irreversibile: chiamata a un metodo non definito CarController :: getReadyToGo ()

Nonostante il fallimento, questo test ci ha dato un punto di partenza per il nostro CarController implementazione. Ho incluso un file, chiamato autoloadCarInterfaces.php, non era nella lista iniziale. Mi sono reso conto che avevo bisogno di qualcosa per caricare le classi e ho scritto una soluzione molto semplice. Possiamo sempre riscriverlo quando vengono fornite le classi reali, ma questa è una storia completamente diversa. Per ora, continueremo con la soluzione facile:

foreach (scandir (dirname (__ FILE__). '/ CarInterface') come $ nomefile) $ path = dirname (__ FILE__). '/ CarInterface /'. $ Nomefile; if (is_file ($ path)) require_once $ path; 

Presumo che questo caricatore di classe sia ovvio per tutti; quindi, parliamo del codice di prova.

Innanzitutto, creiamo un'istanza di CarController, la classe che vogliamo testare. Successivamente, creiamo istanze di tutte le altre classi a cui teniamo: motore, cambio ed elettronica.

Quindi creiamo un manichino Luci oggetto chiamando PHPUnit's getMock () metodo e passando il nome del Luci classe. Questo restituisce un'istanza di Luci, ma ogni metodo ritorna nullo--un oggetto fittizio. Questo oggetto fittizio non può fare nulla, ma fornisce al nostro codice l'interfaccia necessaria per lavorare Luce oggetti.

È molto importante notare questo $ dummyLights è un Luci oggetto e qualsiasi utente si aspetta un Luce l'oggetto può usare l'oggetto fittizio senza sapere che non è reale Luci oggetto.

Per evitare confusione, ti consiglio di specificare il tipo di parametro quando definisci una funzione. Ciò impone al runtime di PHP di digitare controllare gli argomenti passati a una funzione. Senza specificare il tipo di dati, è possibile passare qualsiasi oggetto a qualsiasi parametro, che può causare l'errore del codice. Con questo in mente, esaminiamo il Elettronica classe:

require_once 'Lights.php'; class Electronics function turnOn (Lights $ lights) 

Implementiamo un test:

class CarController function getReadyToGo (Engine $ engine, Gearbox $ gearbox, Elettronica $ electronics, Lights $ lights) $ engine-> start (); $ Gearbox-> spostamento ( 'N'); $ Elettronica-> sfioramento Attiva ($ luci); ritorna vero; 

Come puoi vedere, il getReadyToGo () la funzione ha usato il $ luci oggetto al solo scopo di inviarlo al $ elettronica oggetto di accendere() metodo. È questa la soluzione ideale per una situazione del genere? Probabilmente no, ma puoi chiaramente osservare come un oggetto fittizio, senza alcuna relazione con il getReadyToGo () funzione, viene passato insieme all'unico oggetto che ne ha veramente bisogno.

Si prega di notare che tutte le classi contenute nel CarInterface directory fornisce oggetti fittizi quando inizializzati. Supponiamo anche che, per questo esercizio, ci aspettiamo che il produttore fornisca le classi reali in futuro. Non possiamo fare affidamento sulla loro attuale mancanza di funzionalità; quindi, dobbiamo assicurarci che i nostri test passino.


Passaggio 6: "Stub" lo stato e andare avanti

Un test Stub è un oggetto per controllare l'input indiretto del codice testato. Ma che cos'è l'input indiretto? È una fonte di informazioni che non può essere specificata direttamente.

L'esempio più comune di uno stub di test è quando un oggetto chiede a un altro oggetto informazioni e quindi fa qualcosa con quei dati.

Le spie, per definizione, sono matrici più capaci.

I dati possono essere ottenuti solo richiedendo un oggetto specifico e, in molti casi, questi oggetti vengono utilizzati per uno scopo specifico all'interno della classe testata. Non vogliamo "rinnovarci" (new SomeClass ()) una classe all'interno di un'altra classe a scopo di test. Pertanto, abbiamo bisogno di iniettare un'istanza di una classe che si comporta come SomeClass senza iniettare un reale SomeClass oggetto.

Quello che vogliamo è una classe stub, che poi porta a iniezione di dipendenza. L'iniezione di dipendenza (DI) è una tecnica che inietta un oggetto in un altro oggetto, forzandolo a utilizzare l'oggetto iniettato. DI è comune in TDD ed è assolutamente richiesto in quasi tutti i progetti. Fornisce un modo semplice per forzare un oggetto a utilizzare una classe preparata per il test invece di una classe reale utilizzata nell'ambiente di produzione.

Facciamo andare avanti la nostra macchinina.

Vogliamo implementare un metodo chiamato moveForward (). Questo metodo prima chiede a StatusPanel oggetto per lo stato del carburante e del motore. Se l'auto è pronta, il metodo istruisce l'elettronica ad accelerare.

Per capire meglio come funziona uno stub, scriverò prima il codice per il controllo dello stato e l'accelerazione:

 function goForward (Electronics $ electronics) $ statusPanel = new StatusPanel (); if ($ statusPanel-> engineIsRunning () && $ statusPanel-> thereIsEnoughFuel ()) $ electronics-> accelerate (); 

Questo codice è piuttosto semplice, ma non abbiamo un vero motore o carburante per testare il nostro vai avanti() implementazione. Il nostro codice non entrerà nemmeno nel Se affermazione perché non abbiamo un StatusPanel classe. Ma se continuiamo con il test, una soluzione logica inizia ad emergere:

 function testItCanAccelerate () $ carController = new CarController (); $ electronics = new Electronics (); $ stubStatusPanel = $ this-> getMock ('StatusPanel'); $ StubStatusPanel-> si aspetta ($ this-> qualsiasi ()) -> Metodo ( 'thereIsEnoughFuel') -> volontà ($ this-> returnValue (TRUE)); $ StubStatusPanel-> si aspetta ($ this-> qualsiasi ()) -> Metodo ( 'engineIsRunning') -> volontà ($ this-> returnValue (TRUE)); $ carController-> goForward ($ electronics, $ stubStatusPanel); 

Spiegazione riga per riga:

Adoro la ricorsione; è sempre più facile testare la ricorsione rispetto ai loop.

  • crea un nuovo CarController
  • crea il dipendente Elettronica oggetto
  • creare una simulazione per il StatusPanel
  • aspetto di chiamare thereIsEnoughFuel () zero o più volte e ritorno vero
  • aspetto di chiamare engineIsRunning () zero o più volte e ritorno vero
  • chiamata vai avanti() con Elettronica e StubbedStatusPanel oggetto

Questo è il test che vogliamo scrivere, ma non funzionerà con la nostra attuale implementazione di vai avanti(). Dobbiamo modificarlo:

 function goForward (Elettronica $ elettronica, StatusPanel $ statusPanel = null) $ statusPanel = $ statusPanel? : nuovo StatusPanel (); if ($ statusPanel-> engineIsRunning () && $ statusPanel-> thereIsEnoughFuel ()) $ electronics-> accelerate (); 

La nostra modifica usa iniezione di dipendenza aggiungendo un secondo parametro opzionale di tipo StatusPanel. Determiniamo se questo parametro ha un valore e ne crea uno nuovo StatusPanel Se $ statusPanel è zero. Questo assicura che un nuovo StatusPanel l'oggetto viene creato in produzione, pur continuando a consentirci di testare il metodo.

È importante specificare il tipo di $ statusPanel parametro. Questo assicura che solo a StatusPanel oggetto (o un oggetto di una classe ereditata) può essere passato al metodo. Ma anche con questa modifica, il nostro test non è ancora completo.


Step 7: Completa il test con un vero test di simulazione

Dobbiamo testare una finta Elettronica oggetto per garantire il nostro metodo dal passaggio 6 chiama accelerare(). Non possiamo usare il reale Elettronica classe per diversi motivi:

  • Non abbiamo la classe.
  • Non possiamo verificare il suo comportamento.
  • Anche se potessimo chiamarlo, dovremmo testarlo da solo.

Un test di simulazione è un oggetto che è in grado di controllare sia l'input e l'output indiretti, sia un meccanismo per l'asserzione automatica di aspettative e risultati. Questa definizione può sembrare un po 'confusa, ma è davvero semplice da implementare:

 function testItCanAccelerate () $ carController = new CarController (); $ electronics = $ this-> getMock ('Elettronica'); $ Elettronica-> si aspetta ($ this-> una volta ()) -> Metodo ( 'accelerare'); $ stubStatusPanel = $ this-> getMock ('StatusPanel'); $ StubStatusPanel-> si aspetta ($ this-> qualsiasi ()) -> Metodo ( 'thereIsEnoughFuel') -> volontà ($ this-> returnValue (TRUE)); $ StubStatusPanel-> si aspetta ($ this-> qualsiasi ()) -> Metodo ( 'engineIsRunning') -> volontà ($ this-> returnValue (TRUE)); $ carController-> goForward ($ electronics, $ stubStatusPanel); 

Abbiamo semplicemente cambiato il $ elettronica variabile. Invece di creare un reale Elettronica oggetto, ne prendiamo semplicemente in giro uno.

Nella riga successiva, definiamo un'aspettativa su $ elettronica oggetto. Più precisamente, ci aspettiamo che il accelerare() il metodo è chiamato una sola volta ($ This-> una volta ()). Il test ora passa!

Sentiti libero di giocare con questo test. Prova a cambiare $ This-> una volta () in $ This-> esattamente (2) e vedi che bel messaggio di errore ti dà PHPUnit:

1) CarControllerTest :: testItCanAccelerate Expectation non riuscita per il nome del metodo è uguale a ; quando invocato 2 volte (s). Il metodo avrebbe dovuto essere chiamato 2 volte, in realtà chiamato 1 volta.

Passaggio 8: utilizzare una spia di prova

Una spia di prova è un oggetto in grado di catturare l'output indiretto e fornire input indiretti, se necessario.

L'output indiretto è qualcosa che non possiamo osservare direttamente. Ad esempio: quando la classe testata calcola un valore e quindi lo utilizza come argomento per il metodo di un altro oggetto. L'unico modo per osservare questo output è chiedere all'oggetto chiamato la variabile utilizzata per accedere al suo metodo.

Questa definizione rende una spia quasi una finta.

La principale differenza tra un finto e una spia è che gli oggetti finti hanno asserzioni e aspettative incorporate.

In tal caso, come possiamo creare una spia di prova usando PHPUnit getMock ()? Non possiamo (beh, non possiamo creare una spia pura), ma possiamo creare mistiche capaci di spiare altri oggetti.

Attuiamo il sistema di frenata in modo che possiamo fermare la macchina. La frenata è davvero semplice; il telecomando rileverà l'intensità della frenata da parte dell'utente e lo invierà al controller. Il telecomando fornisce anche un "Arresto di emergenza!" pulsante. Questo deve innestare immediatamente i freni con la massima potenza.

La potenza di frenatura misura valori compresi tra 0 e 100, con 0 che non significa nulla e 100 indica la massima potenza frenante. "Arresto di emergenza!" il comando verrà ricevuto come chiamata diversa.

Il CarController invierà un messaggio al Elettronica oggetto di attivare il sistema frenante. Il controller dell'auto può anche interrogare il StatusPanel per informazioni sulla velocità ottenute tramite sensori sull'auto.

Implementazione usando una pura spia di prova

Iniziamo a implementare un oggetto di spionaggio puro senza utilizzare l'infrastruttura di simulazione di PHPUnit. Questo ti darà una migliore comprensione del concetto di spionaggio del test. Iniziamo controllando il Elettronica firma dell'oggetto.

class Electronics function turnOn (Lights $ lights)  function accelerate ()  function pushBrakes ($ brakingPower) 

Siamo interessati al pushBrakes () metodo. Non l'ho chiamato freno() per evitare confusione con il rompere parola chiave in PHP.

Per creare una vera spia, ci estenderemo Elettronica e scavalcare il pushBrakes () metodo. Questo metodo sovrascritto non spingerà il freno; invece, registrerà solo la potenza di frenata.

classe SpyingElectronics estende l'elettronica private $ brakingPower; funzione pushBrakes ($ brakingPower) $ this-> brakingPower = $ brakingPower;  function getBrakingPower () return $ this-> brakingPower; 

La la getBrakingPower () il metodo ci dà la possibilità di controllare la potenza di frenata nel nostro test. Questo non è un metodo che useremmo nella produzione.

Ora possiamo scrivere un test in grado di testare la potenza di frenata. Seguendo i principi TDD, inizieremo con il test più semplice e forniremo l'implementazione più semplice:

 function testItCanStop () $ halfBrakingPower = 50; $ electronicsSpy = new SpyingElectronics (); $ carController = new CarController (); $ carController-> pushBrakes ($ halfBrakingPower, $ electronicsSpy); $ this-> assertEquals ($ halfBrakingPower, $ electronicsSpy-> getBrakingPower ()); 

Questo test fallisce perché non abbiamo ancora un pushBrakes () metodo attivo CarController. Risolviamolo e scrivine uno:

 function pushBrakes ($ brakingPower, Electronics $ electronics) $ electronics-> pushBrakes ($ brakingPower); 

Il test ora passa, testando efficacemente il pushBrakes () metodo.

Possiamo anche spiare le chiamate ai metodi. Testare il StatusPanel la classe è il prossimo passo logico. Fornisce all'utente diverse informazioni relative all'auto telecomandata. Scriviamo un test che verifica se il StatusPanel oggetto viene chiesto sulla velocità della vettura. Creeremo una spia per questo:

class SpyingStatusPanel estende StatusPanel private $ speedWasRequested = false; function getSpeed ​​() $ this-> speedWasRequested = true;  function speedWasRequested () return $ this-> speedWasRequested; 

Quindi, modifichiamo il nostro test per usare la spia:

 function testItCanStop () $ halfBrakingPower = 50; $ electronicsSpy = new SpyingElectronics (); $ statusPanelSpy = new SpyingStatusPanel (); $ carController = new CarController (); $ carController-> pushBrakes ($ halfBrakingPower, $ electronicsSpy, $ statusPanelSpy); $ this-> assertEquals ($ halfBrakingPower, $ electronicsSpy-> getBrakingPower ()); $ This-> assertTrue ($ statusPanelSpy-> speedWasRequested ()); 

Si noti che non ho scritto un test separato.

La raccomandazione di "una asserzione per test" è buona da seguire, ma quando il test descrive un'azione che richiede diversi passaggi o stati, l'uso di più di una asserzione nello stesso test è accettabile.

Ancora di più, questo mantiene le tue affermazioni su un singolo concetto in un unico posto. Questo aiuta a eliminare il codice duplicato non richiedendo all'utente di impostare ripetutamente le stesse condizioni per il tuo SUT.

E ora l'implementazione:

 function pushBrakes ($ brakingPower, Elettronica $ elettronica, StatusPanel $ statusPanel = null) $ statusPanel = $ statusPanel? : nuovo StatusPanel (); $ Elettronica-> pushBrakes ($ brakingPower); $ StatusPanel-> getSpeed ​​(); 

C'è solo una piccola cosa minuscola che mi infastidisce: il nome di questo test è testItCanStop (). Ciò implica chiaramente che spingiamo i freni finché l'auto non si ferma completamente. Tuttavia, abbiamo chiamato il metodo pushBrakes (), che non è del tutto corretto. Tempo di refactoring:

 funzione stop ($ brakingPower, elettronica $ elettronica, StatusPanel $ statusPanel = null) $ statusPanel = $ statusPanel? : nuovo StatusPanel (); $ Elettronica-> pushBrakes ($ brakingPower); $ StatusPanel-> getSpeed ​​(); 

Non dimenticare di cambiare anche la chiamata al metodo nel test.

$ carController-> stop ($ halfBrakingPower, $ electronicsSpy, $ statusPanelSpy);

L'output indiretto è qualcosa che non possiamo osservare direttamente.

A questo punto, dobbiamo pensare al nostro sistema di frenata e al suo funzionamento. Ci sono diverse possibilità, ma per questo esempio, supponiamo che il fornitore della macchinina specifichi che la frenata avviene in intervalli discreti. Chiamare un Elettronica oggetto di pushBreakes () il metodo spinge il freno per un tempo discreto e poi lo rilascia. L'intervallo di tempo non è importante per noi, ma immaginiamo che sia una frazione di secondo. Con un intervallo di tempo così breve, dobbiamo inviare continuamente pushBrakes () comandi fino a quando la velocità è zero.

Le spie, per definizione, sono matrici più capaci e possono anche controllare l'input indiretto se necessario. Facciamo il nostro StatusPanel spy più capace e offre un certo valore per la velocità. Penso che la prima chiamata dovrebbe fornire una velocità positiva - diciamo il valore di 1. La seconda chiamata fornirà la velocità di 0.

class SpyingStatusPanel estende StatusPanel private $ speedWasRequested = false; private $ currentSpeed ​​= 1; function getSpeed ​​() if ($ this-> speedWasRequested) $ this-> currentSpeed ​​= 0; $ this-> speedWasRequested = true; restituire $ this-> currentSpeed;  function speedWasRequested () return $ this-> speedWasRequested;  function spyOnSpeed ​​() return $ this-> currentSpeed; 

L'override getSpeed ​​() metodo restituisce il valore di velocità appropriato tramite spyOnSpeed ​​() metodo. Aggiungiamo una terza asserzione al nostro test:

 function testItCanStop () $ halfBrakingPower = 50; $ electronicsSpy = new SpyingElectronics (); $ statusPanelSpy = new SpyingStatusPanel (); $ carController = new CarController (); $ carController-> stop ($ halfBrakingPower, $ electronicsSpy, $ statusPanelSpy); $ this-> assertEquals ($ halfBrakingPower, $ electronicsSpy-> getBrakingPower ()); $ This-> assertTrue ($ statusPanelSpy-> speedWasRequested ()); $ this-> assertEquals (0, $ statusPanelSpy-> spyOnSpeed ​​()); 

Secondo l'ultima affermazione, la velocità dovrebbe avere un valore di velocità di 0 dopo il Stop() il metodo termina l'esecuzione. Esecuzione di questo test contro il nostro codice di produzione si traduce in un errore con un messaggio criptico:

1) CarControllerTest :: testItCanStop Fallito affermando che 1 corrispondenze previste sono 0.

Aggiungiamo il nostro messaggio di asserzione personalizzato:

$ this-> assertEquals (0, $ statusPanelSpy-> spyOnSpeed ​​(), 'La velocità prevista è 0 (zero) dopo l'arresto ma in realtà era'. $ statusPanelSpy-> spyOnSpeed ​​());

Ciò produce un messaggio di errore molto più leggibile:

1) CarControllerTest :: testItCanStop La velocità prevista è 0 (zero) dopo l'arresto ma in realtà era 1 Impossibile affermare che 1 corrispondenze previste 0.

Basta fallimenti! Facciamolo passare.

 funzione stop ($ brakingPower, elettronica $ elettronica, StatusPanel $ statusPanel = null) $ statusPanel = $ statusPanel? : nuovo StatusPanel (); $ Elettronica-> pushBrakes ($ brakingPower); if ($ statusPanel-> getSpeed ​​()) $ this-> stop ($ brakingPower, $ electronics, $ statusPanel); 

Adoro la ricorsione; è sempre più facile testare la ricorsione rispetto ai loop. Test più semplici significa codice più semplice, che a sua volta significa un algoritmo migliore. Dai un'occhiata a The Transformation Priority Premise per ulteriori informazioni su questo argomento.

Tornando al framework di simulazione di PHPUnit

Basta con le lezioni extra. Riscriviamolo usando la struttura di derisione di PHPUnit ed eliminiamo quelle purissime spie. Perché?

Perché PHPUnit offre una sintassi mocking migliore e più semplice, meno codice e alcuni buoni metodi predefiniti.

Generalmente creo mogli e mogli puri solo quando li prendi in giro getMock () sarebbe troppo complicato Se le tue lezioni sono così complesse getMock () non è in grado di gestirli, quindi hai un problema con il tuo codice di produzione, non con i tuoi test.

 function testItCanStop () $ halfBrakingPower = 50; $ electronicsSpy = $ this-> getMock ('Elettronica'); $ ElectronicsSpy-> si aspetta ($ this-> esattamente (2)) -> Metodo ( 'pushBrakes') -> con ($ halfBrakingPower); $ statusPanelSpy = $ this-> getMock ('StatusPanel'); $ StatusPanelSpy-> si aspetta ($ this-> a (0)) -> Metodo ( 'getSpeed') -> volontà ($ this-> returnValue (1)); $ StatusPanelSpy-> si aspetta ($ this-> a (1)) -> Metodo ( 'getSpeed') -> volontà ($ this-> returnValue (0)); $ carController = new CarController (); $ carController-> stop ($ halfBrakingPower, $ electronicsSpy, $ statusPanelSpy); 

Il totale di tutti i metodi, pubblici e privati, accessibili tramite metodi pubblici rappresenta il comportamento di un oggetto.

Una spiegazione riga per riga del codice precedente:

  • impostare metà potenza di frenatura = 50
  • creare un Elettronica finto
  • aspetto il metodo pushBrakes () eseguire esattamente due volte con la potenza di frenatura sopra specificata
  • creare un StatusPanel finto
  • ritorno 1 al primo getSpeed ​​() chiamata
  • ritorno 0 al secondo getSpeed ​​() esecuzione
  • chiama il testato Stop() metodo su un reale CarController oggetto

Probabilmente la cosa più interessante in questo codice è la $ This-> a ($ someValue) metodo. PHPUnit conta la quantità di chiamate a quella simulazione. Il conteggio avviene a livello falso; quindi, chiamando più metodi su $ statusPanelSpy incrementerebbe il contatore. Questo può sembrare un po 'contro-intuitivo in un primo momento; quindi diamo un'occhiata a un esempio.

Supponiamo di voler controllare il livello del carburante per ogni chiamata a Stop(). Il codice sarebbe simile a questo:

 funzione stop ($ brakingPower, elettronica $ elettronica, StatusPanel $ statusPanel = null) $ statusPanel = $ statusPanel? : nuovo StatusPanel (); $ Elettronica-> pushBrakes ($ brakingPower); $ StatusPanel-> thereIsEnoughFuel (); if ($ statusPanel-> getSpeed ​​()) $ this-> stop ($ brakingPower, $ electronics, $ statusPanel); 

Questo interromperà il nostro test. Potresti essere confuso perché, ma riceverai il seguente messaggio:

1) CarControllerTest :: testItCanStop Expectation non riuscito per il nome del metodo è uguale a  quando invocato 2 volte (s). Il metodo avrebbe dovuto essere chiamato 2 volte, in realtà chiamato 1 volta.

È abbastanza ovvio pushBrakes () dovrebbe essere chiamato due volte. Perché allora riceviamo questo messaggio? A causa del $ This-> a ($ someValue) aspettativa. Il contatore aumenta come segue:

  • prima chiamata a Stop() -> prima chiamata a thereIsEnougFuel () => contatore interno a 0
  • prima chiamata a Stop() -> prima chiamata a getSpeed ​​() => contatore interno a 1 e ritorno 0
  • seconda chiamata a Stop() mai succede => seconda chiamata a getSpeed ​​() non succede mai

Ogni chiamata a qualunque metodo deriso $ statusPanelSpy incrementa il contatore interno di PHPUnit.


Passo 9: Un falso di prova

Se i metodi pubblici sono simili ai messaggi, i metodi privati ​​sono simili ai pensieri.

Un falso test è un'implementazione più semplice di un oggetto codice di produzione. Questa è una definizione molto simile per testare gli stub. In realtà, Fakes e Stub sono molto simili come da comportamento esterno. Entrambi sono oggetti che imitano il compo