Il mocking parziale

SimpleTest for PHP partial mocks documentation

Il mocking parziale non è nient'altro che un pattern per facilitare la risoluzione di alcuni problemi specifici che si incontrano nel collaudo con oggetti mock e in cui l'uso degli oggetti mock diviene assai difficoltoso. Si tratta di un tool abbastanza limitato e probabilmente non è nemmeno un'idea brillante. E' stato incluso comunque in SimpleTest perché l'ho personalmente trovato utile in più occasioni e mi ha permesso di risparmiare molto tempo..

# inject Il problema del Mock Injection

Quando un oggetto ne uso un altro è semplicissimo passarli una versione mock già decorata con le sue expectation. Le cose diventano molto più complicate quando è un oggetto ne crea un altro e l'oggetto creatore è quello che si deve collaudare. Questo significa che è l'oggetto creato che dovrebbe essere sostituito dalla versione mock: ma può essere difficile impartire alla classe sotto test l'ordine di creare una versione mock invece dell'originale. Del resto, la classe sotto test nemmeno dovrebbe essere a conoscenza di essere eseguita all'interno di un test.

Per esempio, supponiamo che si stia scrivendo un client telnet e che ci sia la necessità di creare un socket a cui passare i messaggi. Il metodo di connessione potrebbe avere un aspetto simile a questo: <strong><?php require_once('socket.php'); class Telnet { ... function &connect($ip, $port, $username, $password) { $socket = &new Socket($ip, $port); $socket->read( ... ); ... } } ?></strong> Se volessimo avere una versione mock del socket, cosa si potrebbe fare?

La prima soluzione è quella di passare il socket come parametro, in mododi forzare la creazione al livello superiore. Il fatto che sia il client a dover gestire la creazione è un buon approccio se si è disposti a gestire la cosa e dovrebbe spingere verso una fattorizazione della creazione. Difatti, questo è uno dei sistemi con i quali il test con i mock object costringono lo sviluppatore a sviluppare adottando soluzioni più rigorose. I mock migliorano lo stile di programmazione.

La soluzione potrebbe quindi essere: <?php require_once('socket.php'); class Telnet { ... <strong>function &connect(&$socket, $username, $password) { $socket->read( ... ); ... }</strong> } ?> Ciò significa che il codice di test si riduce all'invocazione tipica degli oggetti mock: class TelnetTest extends UnitTestCase { ... function testConnection() {<strong> $socket = &new MockSocket($this); ... $telnet = &new Telnet(); $telnet->connect($socket, 'Me', 'Secret'); ...</strong> } } E' abbastanza evidente, tuttavia, che un unico livello è tutto quello che ci si può aspettare di ottenere. Difficilmente accetteremo che l'applicazione stessa debba preoccuparsi della creazione di socket, connessione al database ed altri oggetti necessari ai livelli sottostanti. Sarebbe difficile anche conoscere i parametri necessari ai costruttori.

Un possibilecompromesso è quello di prevedere l'oggetto istanziato come parametro opzionale: <?php require_once('socket.php'); class Telnet { ...<strong> function &connect($ip, $port, $username, $password, $socket = false) { if (!$socket) { $socket = &new Socket($ip, $port); } $socket->read( ... );</strong> ... return $socket; } } ?> Come soluzione di emergenza questo può anche andare bene. Adesso il test funziona come se il parametro fosse formalmente passato: class TelnetTest extends UnitTestCase { ... function testConnection() {<strong> $socket = &new MockSocket($this); ... $telnet = &new Telnet(); $telnet->connect('127.0.0.1', 21, 'Me', 'Secret', &$socket); ...</strong> } } Il problema di questo approccio è nel disordine che genera. Nella classe principale c'è del codice di test e nel codice di test ci sono dei paramtri che non vengono mai utilizzati. E' un approccio veloce e sporco, ma nonostante tutto può essere efficace in molte situazioni.

Un'ulteriore soluzione è quella di passare un oggetto factory a cui demandare la creazione: <?php require_once('socket.php'); class Telnet {<strong> function Telnet(&$network) { $this->_network = &$network; }</strong> ... function &connect($ip, $port, $username, $password) {<strong> $socket = &$this->_network->createSocket($ip, $port); $socket->read( ... );</strong> ... return $socket; } } ?> Questa è probabilmente la soluzione più fattorizzata dal momento che la creazione degli oggetti è stata spostata all'interno di una piccola classe specializzata. Adesso il factory della rete può essere collaudato separatamente dal resto ma risulta anche semplice crearne un mock nel momento del collaudo della classe telnet: class TelnetTest extends UnitTestCase { ... function testConnection() {<strong> $socket = &new MockSocket($this); ... $network = &new MockNetwork($this); $network->returnsByReference('createSocket', $socket); $telnet = &new Telnet($network); $telnet->connect('127.0.0.1', 21, 'Me', 'Secret'); ...</strong> } } Il rovescio della medaglia è che si sono aggiunte molte classi alla libreria e che si stanno passando molte classi factory in qua e là ed il codice diventa un po' meno intuitivo. Questa è la soluzione più flessibile ma anche la più complessa.

Esiste una via di mezzo?

# creation Il pattern Protected Factory

Esiste un sistema con cui è possibile circoscrivere il problema senza creare nuove classi ma prevede l'utilizzo di una sottoclasse della classe originale nel test. Intanto, spostiamo la creazione del socket in un metodo dedicato: <?php require_once('socket.php'); class Telnet { ... function &connect($ip, $port, $username, $password) {<strong> $socket = &$this->_createSocket($ip, $port);</strong> $socket->read( ... ); ... }<strong> function &_createSocket($ip, $port) { return new Socket($ip, $port); }</strong> } ?> Questo è l'unico cambiamento richiesto al codice dell'applicazione.

Nel test case si deve creare una sottoclasse in modo da poter intercettare la creazione del socket: <strong>class TelnetTestVersion extends Telnet { var $_mock; function TelnetTestVersion(&$mock) { $this->_mock = &$mock; $this->Telnet(); } function &_createSocket() { return $this->_mock; } }</strong> Qui il mock è stato passato al costruttore ma un ordinario metodo setter avrebbe svolto ugualmente il medesimo compito. Si noti come il mock è stato iniettato dentro l'oggetto prima della chiamata del costruttore. Questo è necessario nel caso il costruttore faccia chiamate a `connect()`. In caso contrario `_createSocket()` avrebbe restituito al costruttore un valore nullo

Dopo il completamento di tutto questo lavoro extra, il test in se' è diascretamente più semplice. E' sufficiente colaudare la nuova classe invece di quella originale: class TelnetTest extends UnitTestCase { ... function testConnection() {<strong> $socket = &new MockSocket($this); ... $telnet = &new TelnetTestVersion($socket); $telnet->connect('127.0.0.1', 21, 'Me', 'Secret'); ...</strong> } } La nuova classe, naturalmente, è molto semplice. Semplicemente imposterà un valore di ritorno, come un mock. Sarebbe bello se, oltre a questo, verificasse anche i parametri ricevuti. Proprio come un mock. Visto che sembra useremo spesso questa tecnica, possiamo automatizzare la creazione della sottoclasse?

# partial Un mock parziale

Naturalmente la risposta è "sì", altrimenti avrei smesso di scrivere da un pezzo! L'ultimo test che abbiamo scritto ha comportato un bel po' di lavoro ma si potrebbe utilizzare un approccio simile alla generazione degli oggetti mock per creare la sottoclasse.

Questa è la versione del test con il mocking parziale: <strong>Mock::generatePartial( 'Telnet', 'TelnetTestVersion', array('_createSocket'));</strong> class TelnetTest extends UnitTestCase { ... function testConnection() {<strong> $socket = &new MockSocket($this); ... $telnet = &new TelnetTestVersion($this); $telnet->setReturnReference('_createSocket', $socket); $telnet->Telnet(); $telnet->connect('127.0.0.1', 21, 'Me', 'Secret'); ...</strong> } } Il mock parziale è una sottoclasse della classe originale nella quale alcuni metodi sono sostituiti con versioni di test. `generatePartial()` accetta tre parametri: la classe da estendere, il nome dellla nuova classe di test ed un elenco dei metodi da sostituire.

Il modo di istanziare gli oggetti risultati è un po' delicato. L'unico parametro atteso dal costruttore di un mock parziale è un riferimento allo unit test. Così come gli ordinari oggetti mock, infatti, questo è necesssario affinché l'oggetto possa restituire i risultati delle expectation valutate.

Il costruttore originale non è stato ancora invocato. E' necessario rimandare la sua invocazione perché potrebbe fare uso di uno dei metodi sostituiti ancora non impostati. Si impostano quindi i valori di ritorno e solo a questo punto si invoca il costruttore con i suoi parametri. Quello che distingue i mock parziali dai mock ordinari è la successione di questi tre passi: costruzione con "new", impostazione dei metodi, invocazione del costruttore vero e proprio.

Fatta esclusione del processo di costruzione, tutti i metodi sostituiti si comportano come stabilito dal mock mentre tutti i metodi originari continuano a comportarsi come nell'oggetto originario. E' molto semplice impostare le expectation: class TelnetTest extends UnitTestCase { ... function testConnection() { $socket = &new MockSocket($this); ... $telnet = &new TelnetTestVersion($this); $telnet->setReturnReference('_createSocket', $socket);<strong> $telnet->expectOnce('_createSocket', array('127.0.0.1', 21));</strong> $telnet->Telnet(); $telnet->connect('127.0.0.1', 21, 'Me', 'Secret'); ...<strong> $telnet->tally();</strong> } }

# less Collaudare meno di una classe

Non è necessario che i metodi sostituiti siano metodi factory: è possibile sostituire qualsiasi tipologia di metodo. Questo è il modo in cui i mock parziali permettono di prendere il controllo di qualsiasi parte di una classe, eslcuso il costruttore. Sarebbe perfino possibile sostituire tutti i metodi tranne l'unico da collaudare.

Quest'ultima situazione è piuttosto ipotetica dal momento che non l'ho mai provata. Posso anche credere che questo funzioni ma sono convinto che forzando la granularità dell'oggetto non si ottengano necessariamente un codice di qualità migliore. Personalmente utilizzo il mocking parziale come un sistema per l'override della creazione di oggetti o per l'occasionale test di oggetti che implementino il design TemplateMethod.

Quale meccanismo utilizzare, alla fine, è una questione legata allo standard che si è scelto per i propri progetti.

The mock injection problem. Moving creation to a protected factory method. Partial mocks generate subclasses. Partial mocks test less than a class.
SimpleTest project page on SourceForge. Full API for SimpleTest from the PHPDoc. The protected factory is described in this paper from IBM. This is the only formal comment I have seen on this problem.
php software development, php test case development, database programming php, software development tools, php advanced tutorial, phpunit style scripts, architecture, php resources, mock objects, junit, php test framework, unit test, php testing