Le classi boundary

PHP unit testing tutorial - Organising unit tests and "setup tests"

Probabilmente starai pensando che abbiamo esaurito ogni argomento a proposito della classe `Log` e che adesso non c'è davvero niente da aggiungere. Ma le cose non sono mai così semplici nella programmazione orientata agli oggetti. Pensi di aver compreso un problema quando sopraggiunge qualcosa che sfida le tue convinzioni e ti porta ad una comprensione ancora più profonda. Credevo di aver capito la classe log e che l'avrei citata solo nella prima pagina del tutorial per dedicarmi in seguito a qualcosa di più complesso. Nessuno è più sopreso di me nel non riuscire ancora a vederne il fondo. In effetti, penso di aver solo dato una vaga idea di cosa faccia una classe per il logging.

# variation Gestire le variazioni nel logger.

Supponiamo che non si voglia affatto fare il log su un file. Forse desideriamo stampare il messaggio sullo schermo o scriverlo su un socket o spedirmo al demone syslog di Unix(tm) perché sia distribuito sulla rete. Come possiamo incorporare queste variazioni?

Il modo più semplice è quello di fare il subclassing di `Log` facendo l'overring del metodo `message()` con una nuova versione. Funziona a breve termine ma c'è qualcosa di sottile e di profondamente sbagliato in questo. Supponiamo di fare il subclassing e di ottenere i logger per scrivere su file, sullo schermo e sulla rete. Tre classi, ma va bene così. Adesso supponiamo che si voglia una nuova classe di log che aggiunga la funzionalità di filtro in base alla priorità per consentire solo alcuni tipi di messaggio in funzione di un qualche file di configurazione.

Siamo incastrati. Se applichiamo di nuovo il subclassing dobbiamo farlo per 3 classi e finiamo per ottenerne 6. La quantità di duplicazione è orribile.

Desideriamo forse che PHP avesse l'ereditarietà multipla? Be', questo ridurrebbe il lavoro, a breve termine ma complicherebbe quella che dovrebbe essere una classe molto semplice. L'ereditarietà multipla, anche quando è supportata, dovrebbe essere usata con estrema cautela perché porta ad ogni tipo di complicazione. Consideralo una pistola carico. Infatti, la nostra necessità immediata è quella di ammetere qualcos'altro: forse che abbiamo sbagliato qualcosa a livello concettuale.

Cosa fa un logger? Invia messaggi ad un file? Invia messaggi alla rete? Invia messaggi ad uno schermo? No. Semplicemente invia messaggi. Punto. Il destinatario di questi messaggi può anche essere sceltro durante l'impostazione del logger ma, dopo tutto, il logger dovrebbe limitarsi a combinare e formattare i messaggi perché solo questo è il suo vero compito. Prima ci siamo posti delle restrizioni assumendo che il destinatario fosse un file.

# writer Astrazione di Writer

La soluzione per questa triste situazione è un vero classico. Primo, incapsuliamo la variazione in una classe così da aggiungere un livello di isolamento. Invece di passare il nome del file come stringa, passeremo "la cosa a cui bisogna inviare i messaggi" che potremmo chiamare `Writer`. Torniamo ai test: <?php require_once('simpletest/autorun.php'); require_once('../classes/log.php'); require_once('../classes/clock.php');<strong> require_once('../classes/writer.php');</strong> Mock::generate('Clock'); class TestOfLogging extends UnitTestCase { function setUp() { @unlink('../temp/test.log'); } function tearDown() { @unlink('../temp/test.log'); } function testCreatingNewFile() {<strong> $log = new Log(new FileWriter('../temp/test.log'));</strong> $this->assertFalse(file_exists('../temp/test.log'), 'Created before message'); $log->message('Should write this to a file'); $this->assertTrue(file_exists('../temp/test.log'), 'File created'); } function testAppendingToFile() {<strong> $log = new Log(new FileWriter('../temp/test.log'));</strong> $log->message('Test line 1'); $this->assertPattern( '/Test line 1/', $this->getFileLine('../temp/test.log', 0)); $log->message('Test line 2'); $this->assertPattern( '/Test line 2/', $this->getFileLine('../temp/test.log', 1)); } function testTimestamps() { $clock = new MockClock($this); $clock->returns('now', 'Timestamp');<strong> $log = new Log(new FileWriter('../temp/test.log'));</strong> $log->message('Test line', $clock); $this->assertPattern( '/Timestamp/', $this->getFileLine('../temp/test.log', 0), 'Found timestamp'); } function getFileLine($filename, $index) { $messages = file($filename); return $messages[$index]; } } ?> Procederò un passo alla volta in modo da non confonderci. Ho rimpiazzato il nome dei file con un'immaginaria classe `FileWriter` definita in un immaginario file classes/writer.php. Questo farà andare in crash i test dal momento che non abbiamo ancora scritto il writer. Dobbiamo farlo subito?

Potremmo, ma non è obbligatorio. Piuttosto, abbiamo bisogno di scriverne l'interfaccia o non avremo modo di fare il mock. Ecco come appare classes/writer.php: <?php class FileWriter { function FileWriter($file_path) { } function write($message) { } } ?> Dobbiamo modificare anche la classe `Log`: <?php require_once('../classes/clock.php');<strong> require_once('../classes/writer.php');</strong> class Log {<strong> private $writer;</strong> function Log(<strong>$writer</strong>) {<strong> $this->writer = $writer;</strong> } function message($message, $clock = false) { if (! is_object($clock)) { $clock = new Clock(); }<strong> $this->writer->write("[" . $clock->now() . "] $message");</strong> } } ?> Non c'è molto che sia rimasto inalterato nella nostra nuova classe, più piccola di prima. I test si avviano ma continueranno a fallire a questo punto fino a che non aggiungeremo il codice al writer. Cosa possiamo fare?

Potremmo iniziare a scrivere i test ed il codice di `FileWriter` insieme, ma nel frattempo i test di `Log` continuerebbero a fallire e a distrarre la nostra attenzione. In effetti non è quello che dobbiamo fare.

Uan parte del nostro piano è liberare la classe di logging dal filesystem e c'è un modo per ottenerlo. Per prima cosa agiungiamo un file tests/writer_test.php così potremo disporre di un posto per ospitare il codice preso da log_test.php che ci stiamo apprestando a mescolare. Non lo aggiungerò ancora al file all_tests.php file perché è il logging l'argomento da affrontare al momento.

Adesso che l'ho fatto (onestamente!) elimino tutti i test da log_test.php che non siano strettamente corrrelati all'attività di logging e li sposto in writer_test.php per uso futuro. Creo anche il mock del writer così che non scriva veramente su un file: <?php require_once('simpletest/autorun.php'); require_once('../classes/log.php'); require_once('../classes/clock.php'); require_once('../classes/writer.php'); Mock::generate('Clock');<strong> Mock::generate('FileWriter');</strong> class TestOfLogging extends UnitTestCase { <strong> function testWriting() { $clock = new MockClock(); $clock->returns('now', 'Timestamp'); $writer = new MockFileWriter($this); $writer->expectOnce('write', array('[Timestamp] Test line')); $log = new Log($writer); $log->message('Test line', $clock); }</strong> } ?> Sì, questo è davvero il test case nella sua interezza ed è veramente così corto. Sono successe molte cose:

1. Il requisito di creare un file solo quando necessario è stato spostato in `FileWriter`.
2. Stiamo lavorando con dei mock: nessun file viene fisicamente creato e, quindi, ho spostato `setUp()` e `tearDown()` dentro il test di `FileWriter`.
3. Il test adesso consiste nell'invio di un messaggio di esempio e nella verifica del formato.

Fermi un attimo: dove sono gli assert?

Gli oggetti mock fanno molto più che simulare il comportamento di altri oggetti: eseguono anche i test.

L'invocazione di `expectOnce()` comunica al mock di attendersi come unico parametro la stringa "[Timestamp] Test line" quando il metodo `write()` viene chiamato. Quando il metodo viene invocato, il parametro atteso viene confrontato con questo e un messaggio di successo o di fallimento viene inviato allo unit test come risultato.

L'altro comportamento atteso è che `write()` sia chiamato una ed una sola volta. Nel caso il metodo non venga invocato entro la fine del test viene generato un fallimento. Possiamo vedere tutto questo in azione eseguendo i test:

# All tests

Pass: log_test.php->Log class test->testwriting->Arguments for [write] were [String: [Timestamp] Test line]
Pass: log_test.php->Log class test->testwriting->Expected call count for [write] was [1], but got [1]
Pass: clock_test.php->Clock class test->testclockadvance->Advancement
Pass: clock_test.php->Clock class test->testclocktellstime->Now is the right time
3/3 test cases complete. 4 passes and 0 fails.

# boundary Isolare i setup test

Apparte l'essersi ridotto di dimensioni, qualcosa di molto piacevole è accaduto al logger.

Gli unici elementi da cui adesso dipende sono le classi che abbiamo direttamente scritto e queste, nei test, sono sostituite da mock così che non esiste più alcuna dipendenza se non dal nostro stesso codice PHP. Non c'è scrittura su file o attesa di ticchetii di orologio. In altre parole, il test case log_test.php verrà eseguito alla massima velocità supportata dal processore. Al contrario, le classi `FileWriter` e `Clock` sono molto legate al sistema e questo le rende più difficili da collaudare perché i dati reali devono essere spostati ed assiduamente controllati, spesso con trucchi ad hoc.

L'ultimo refactoring che abbiamo effettuato ci è stato utile. La difficoltà nel collaudare le classi all'interfaccia tra l'applicazione ed il sistema sono stati ridotti dal momento che il codice di I/O e stato ulteriormente sepatato dalla logica di dominio. Esistono delle corrispondenze dirette con le operazioni PHP: `FileWriter::write()` corrisponde al comando PHP `fwrite()` con il file aperto in modalità append e `Clock::now()` corrisponde a `time()`. Questo facilita enormemente il debugging. Significa anche che queste classi cambieranno più raramente.

Se non cambieranno spesso allora non c'è ragione per continuare ad eseguire i loro test. Ciò implica che i test per le classi di interfaccia possono essere trasferite in test suite dedicate in modo da permettere alle test suite delle unità di essere eseguite al massimno della velocità. In effeti è quello che cerco di fare e gli stessi test case di SimpleTest sono suddivisi in questo modo.

Può non sembrare molto per il caso di uno unit test con due test di interfaccia ma, tipicamente, le applicazioni hanno decine di classi di interfaccia e centinaia di classi di applicazione. Per garantire che le unit test vengano eseguite al massimo della velocità si cercherà sempre di mantenerle separate.

Un ulteriore vantaggio di questa separazione è che in tutti i casi di dipendenza si ottengono test suite più ridotte. Supponiamo si voglia impostare un server per l'applicazione e che ci si voglia assicurare che le componenti sottostanti, le directory e le configurazioni siano corretti. Non ci sarà il bisogno di eseguire tutti i test: sarà sufficiente eseguire i test delle classi di interfaccia. Per questo motivo la test suite per lo scopo verrà chiamata "setup test". Consegnala al tuo amministratore di sistema e lascia che venga utilizzata per configurare il server per te.

Apparte questo, isolare le decisioni di quali componenti del sistema utilizzare è una buona pratica nello sviluppo. Possedere l'abilità di modificare le dipendenze sottostanti con facilità è un bene. E' possibile che il mocking una pratica che migliora il design?

Gestire le variazioni nel logger. Aumentare l'astrazione con una classe mock di Writer. Isolare i setup test per mantenere l'ordine.
This tutorial follows the Mock objects introduction. Next is test driven design. You will need the SimpleTest testing framework to try these examples.
software development, php programming, programming php, software development tools, php tutorial, free php scripts, organizing unit tests, testing tips, development tricks, software architecture for testing, php example code, mock objects, junit port, test case examples, php testing, unit test tool, php test suite