I framework forniscono uno strumento per lo sviluppo rapido di applicazioni, ma spesso maturano il debito tecnico con la stessa rapidità con cui consentono di creare funzionalità. Il debito tecnico viene creato quando la manutenibilità non è un obiettivo intenzionale dello sviluppatore. I cambiamenti futuri e il debug diventano costosi, a causa della mancanza di test e strutture unitarie.
Ecco come iniziare a strutturare il codice per ottenere testabilità e manutenibilità e risparmiare tempo.
Iniziamo con un codice inventato, ma tipico. Questa potrebbe essere una classe di modello in ogni dato framework.
class User public function getCurrentUser () $ user_id = $ _SESSION ['user_id']; $ user = App :: db-> select ('id, username') -> where ('id', $ user_id) -> limit (1) -> get (); if ($ user-> num_results ()> 0) return $ user-> row (); return false;
Questo codice funzionerà, ma ha bisogno di miglioramenti:
$ _SESSION
variabile globale. I framework di test unitari, come PHPUnit, si basano sulla riga di comando, dove $ _SESSION
e molte altre variabili globali non sono disponibili.App :: db
usato nella nostra applicazione. Inoltre, per quanto riguarda le istanze in cui non desideriamo solo le informazioni dell'utente corrente?Ecco un tentativo di creare un unit test per la funzionalità di cui sopra.
class UserModelTest estende PHPUnit_Framework_TestCase public function testGetUser () $ user = new User (); $ currentUser = $ user-> getCurrentUser (); $ this-> assertEquals (1, $ currentUser-> id);
Esaminiamo questo. Innanzitutto, il test fallirà. Il $ _SESSION
variabile usata nel Utente
l'oggetto non esiste in un test unitario, poiché esegue PHP nella riga di comando.
In secondo luogo, non vi è alcuna configurazione di connessione al database. Ciò significa che, al fine di rendere questo lavoro, avremo bisogno di riavviare la nostra applicazione al fine di ottenere il App
oggetto e suo db
oggetto. Avremo anche bisogno di una connessione al database funzionante per testare contro.
Per fare funzionare questo test unitario, dovremmo:
Quindi, passiamo al modo in cui possiamo migliorare questo.
La funzione che recupera l'utente corrente non è necessaria in questo semplice contesto. Questo è un esempio forzato, ma nello spirito dei principi DRY, la prima ottimizzazione che scelgo di fare è generalizzare questo metodo.
class User funzione pubblica getUser ($ user_id) $ user = App :: db-> select ('user') -> where ('id', $ user_id) -> limit (1) -> get (); if ($ user-> num_results ()> 0) return $ user-> row (); return false;
Questo fornisce un metodo che possiamo usare in tutta la nostra intera applicazione. Possiamo passare l'utente corrente al momento della chiamata, piuttosto che passare tale funzionalità al modello. Il codice è più modulare e mantenibile quando non si basa su altre funzionalità (come la variabile globale della sessione).
Tuttavia, questo non è ancora testabile e manutenibile come potrebbe essere. Stiamo ancora facendo affidamento sulla connessione al database.
Aiutiamo a migliorare la situazione aggiungendo un po 'di Iniezione delle Dipendenze. Ecco come potrebbe apparire il nostro modello, quando passiamo la connessione al database nella classe.
class User protected $ _db; funzione pubblica __construct ($ db_connection) $ this -> _ db = $ db_connection; public function getUser ($ user_id) $ user = $ this -> _ db-> select ('user') -> where ('id', $ user_id) -> limit (1) -> get (); if ($ user-> num_results ()> 0) return $ user-> row (); return false;
Ora, le dipendenze del nostro Utente
modello sono previsti. La nostra classe non assume più una determinata connessione al database, né si basa su alcun oggetto globale.
A questo punto, la nostra classe è fondamentalmente testabile. Possiamo passare in una fonte di dati di nostra scelta (principalmente) e un id utente e testare i risultati di tale chiamata. Possiamo anche cambiare connessioni di database separate (supponendo che entrambi implementino gli stessi metodi per il recupero dei dati). Freddo.
Diamo un'occhiata a cosa potrebbe sembrare un test unitario per questo.
_mockDb (); $ user = new User ($ db_connection); $ risultato = $ utente-> getUser (1); $ expected = new StdClass (); $ expected-> id = 1; $ expected-> username = 'fideloper'; $ this-> assertEquals ($ result-> id, $ expected-> id, 'User ID impostato correttamente'); $ this-> assertEquals ($ result-> username, $ expected-> username, 'Username impostato correttamente'); protected function _mockDb () // "Mock" (stub) oggetto risultato riga del database $ returnResult = new StdClass (); $ returnResult-> id = 1; $ returnResult-> username = 'fideloper'; // Oggetto risultato database fittizio $ result = m :: mock ('DbResult'); $ result-> shouldReceive ('num_results') -> once () -> andReturn (1); $ result-> shouldReceive ('row') -> once () -> andReturn ($ returnResult); // Oggetto di connessione database fittizio $ db = m :: mock ('DbConnection'); $ db-> shouldReceive ('select') -> once () -> andReturn ($ db); $ db-> shouldReceive ('where') -> once () -> andReturn ($ db); $ db-> shouldReceive ('limit') -> once () -> andReturn ($ db); $ db-> shouldReceive ('get') -> once () -> andReturn ($ result); return $ db;
Ho aggiunto qualcosa di nuovo a questo test unitario: Mockery. Mockery ti permette di "falsificare" (falsi) oggetti PHP. In questo caso, stiamo prendendo in giro la connessione al database. Con la nostra simulazione, possiamo saltare il test di una connessione al database e semplicemente testare il nostro modello.
Vuoi saperne di più su Mockery?
In questo caso, stiamo prendendo in giro una connessione SQL. Stiamo dicendo all'oggetto finto di aspettarsi di avere il selezionare
, dove
, limite
e ottenere
metodi chiamati su di esso. Sto restituendo il Mock, a sua volta, per rispecchiare il modo in cui l'oggetto di connessione SQL restituisce sé stesso ($ questo
), rendendo così il suo metodo chiamato "concatenabile". Si noti che, per il ottenere
metodo, restituisco il risultato della chiamata del database - a stdClass
oggetto con i dati utente popolati.
Questo risolve alcuni problemi:
Possiamo ancora fare molto meglio. Ecco dove diventa interessante.
Per migliorare ulteriormente, potremmo definire e implementare un'interfaccia. Considera il seguente codice.
interfaccia UserRepositoryInterface getUser funzione pubblica ($ user_id); classe MysqlUserRepository implementa UserRepositoryInterface protected $ _db; funzione pubblica __construct ($ db_conn) $ this -> _ db = $ db_conn; public function getUser ($ user_id) $ user = $ this -> _ db-> select ('user') -> where ('id', $ user_id) -> limit (1) -> get (); if ($ user-> num_results ()> 0) return $ user-> row (); return false; class User protected $ userStore; funzione pubblica __construct (UserRepositoryInterface $ user) $ this-> userStore = $ user; public function getUser ($ user_id) return $ this-> userStore-> getUser ($ user_id);
Ci sono alcune cose che accadono qui.
Aggiungi utente()
metodo.Interfaccia utente
nel nostro Utente
modello. Ciò garantisce che l'origine dati abbia sempre un getUser ()
metodo disponibile, indipendentemente dall'origine dati utilizzata per l'implementazione Interfaccia utente
.Nota che il nostro
Utente
tipo di oggetto-suggerimentiInterfaccia utente
nel suo costruttore. Ciò significa che una classe sta implementandoInterfaccia utente
DEVE essere passato nelUtente
oggetto. Questa è una garanzia su cui facciamo affidamento: abbiamo bisogno delgetUser
metodo per essere sempre disponibile.
Qual è il risultato di questo?
Utente
classe, possiamo facilmente prendere in giro la fonte dei dati. (Testare le implementazioni dell'origine dati sarebbe il lavoro di un test unitario separato).Utente
oggetto se abbiamo bisogno di Se decidi di abbandonare SQL, puoi semplicemente creare un'implementazione diversa (ad esempio, MongoDbUser
) e passalo al tuo Utente
modello.Abbiamo semplificato anche il nostro test unitario!
_mockUserRepo (); $ user = new User ($ userRepo); $ risultato = $ utente-> getUser (1); $ expected = new StdClass (); $ expected-> id = 1; $ expected-> username = 'fideloper'; $ this-> assertEquals ($ result-> id, $ expected-> id, 'User ID impostato correttamente'); $ this-> assertEquals ($ result-> username, $ expected-> username, 'Username impostato correttamente'); funzione protetta _mockUserRepo () // Mock risultato atteso $ result = new StdClass (); $ result-> id = 1; $ result-> username = 'fideloper'; // Mock qualsiasi repository utente $ userRepo = m :: mock ('Fideloper \ Third \ Repository \ UserRepositoryInterface'); $ userRepo-> shouldReceive ('getUser') -> once () -> andReturn ($ result); return $ userRepo;
Abbiamo preso il lavoro di deridere una connessione al database completamente. Invece, semplicemente prendiamo in giro la fonte dei dati e diciamo cosa fare quando getUser
è chiamato.
Ma possiamo ancora fare meglio!
Considera l'uso del nostro codice attuale:
// In alcuni controller $ user = new User (new MysqlUser (App: db-> getConnection ("mysql"))); $ user-> id = App :: session ("user-> id"); $ currentUser = $ user-> getUser ($ user_id);
Il nostro passo finale sarà presentare contenitori. Nel codice precedente, abbiamo bisogno di creare e utilizzare un gruppo di oggetti solo per ottenere il nostro utente corrente. Questo codice potrebbe essere disseminato nella tua applicazione. Se hai bisogno di passare da MySQL a MongoDB, lo farai ancora è necessario modificare ogni posizione in cui viene visualizzato il codice sopra. Quello è appena ASCIUTTO. I contenitori possono risolvere questo problema.
Un contenitore semplicemente "contiene" un oggetto o funzionalità. È simile a un registro nella tua applicazione. Possiamo usare un contenitore per istanziare automaticamente un nuovo Utente
oggetto con tutte le dipendenze necessarie. Sotto, io uso Pimple, una classe contenitore popolare.
// Da qualche parte in un file di configurazione $ container = new Pimple (); $ container ["user"] = function () return new User (new MysqlUser (App: db-> getConnection ('mysql'))); // Ora, in tutti i nostri controller, possiamo semplicemente scrivere: $ currentUser = $ container ['user'] -> getUser (App :: session ('user_id'));
Ho spostato la creazione di Utente
modello in un'unica posizione nella configurazione dell'applicazione. Di conseguenza:
Utente
oggetto e l'archivio dati di scelta sono definiti in un'unica posizione nella nostra applicazione.Utente
modello dall'uso di MySQL a qualsiasi altra origine dati in UNO Posizione. Questo è molto più gestibile.Nel corso di questo tutorial, abbiamo realizzato quanto segue:
Sono sicuro che hai notato che abbiamo aggiunto molto più codice nel nome della manutenibilità e testabilità. Un argomento forte può essere fatto contro questa implementazione: stiamo aumentando la complessità. In effetti, ciò richiede una conoscenza più approfondita del codice, sia per l'autore principale che per i collaboratori di un progetto.
Tuttavia, il costo della spiegazione e della comprensione è di gran lunga superfluo nel complesso diminuire in debito tecnico.
Puoi includere Presa in giro e PHPUnit nella tua applicazione facilmente usando Composer. Aggiungi questi alla tua sezione "require-dev" nella tua composer.json
file:
"require-dev": "mockery / mockery": "0.8. *", "phpunit / phpunit": "3.7. *"
È quindi possibile installare le dipendenze basate su Composer con i requisiti "dev":
$ php composer.phar installa --dev
Ulteriori informazioni su Mockery, Composer e PHPUnit qui su Nettuts+.
Per PHP, considera l'utilizzo di Laravel 4, in quanto fa un uso eccezionale di contenitori e altri concetti scritti qui.
Grazie per aver letto!