SlideShare une entreprise Scribd logo
1  sur  39
Télécharger pour lire hors ligne
1 L’esercizio
L’esercizio che risolvo in queste pagine è il TripServiceKata di Sandro Mancuso
(https://github.com/sandromancuso/trip-service-kata).
In questo esercizio si prende del codice legacy esistente, lo si mette sotto test e lo si rifattorizza per
migliorarne il design.
L’esercizio è già stato risolto più volte in pubblico dallo stesso Mancuso e in rete si trovano i filmati
dove Mancuso mostra la sua soluzione. Quella che riporto è la mia soluzione, riprende molto
dell’originale ma ci sono anche delle differenze. La prima differenza è il linguaggio di
programmazione, l’originale di Mancuso usava il Java io uso il PHP, questo mi permette di mostrare
un paio di cose che in Java non si possono usare.
Se volete vedere la soluzione originale di Mancuso potete andare a farlo su YouTube:
https://www.youtube.com/watch?v=_NnElPO5BU0
Se volete vedere come la faccio io continuare di seguito.
2 Il codice legacy da testare
L’obiettivo dell’esercizio è riuscire a testare il codice legacy mostrato sotto.
<?php
namespace TripServiceKataTrip;
use TripServiceKataUserUser;
use TripServiceKataUserUserSession;
use TripServiceKataExceptionUserNotLoggedInException;
class TripService
{
public function getTripsByUser(User $user) {
$tripList = array();
$loggedUser = UserSession::getInstance()->getLoggedUser();
$isFriend = false;
if ($loggedUser != null) {
foreach ($user->getFriends() as $friend) {
if ($friend == $loggedUser) {
$isFriend = true;
break;
}
}
if ($isFriend) {
$tripList = TripDAO::findTripsByUser($user);
}
return $tripList;
} else {
throw new UserNotLoggedInException();
}
}
}
È una classe che implementa un servizio di un Social Network per viaggiatori. In sintesi i requisiti
di questo servizio sono i seguenti:
• solo gli utenti loggati possono accedere al servizio
• il servizio permette di vedere i viaggi degli altri utenti
• si possono vedere i viaggi dei soli utenti “amici”, i viaggi degli sconosciuti non si possono
vedere
3 Testare il framework di testing
Il primo test che lancio è sempre il test stupido. Cioè un test che sono sicuro fallisca e che fallisca
con un messaggio che conosco bene.
class TripServiceTest extends TestCase
{
function test_something() {
self::assertEquals(1,2);
}
}
Controllo che 1 è uguale a 2. Sono sicuro che fallisca, e, se tutto è configurato bene, dovrebbe darmi
un messaggio come quello di seguito:
Failed asserting that 2 matches expected 1.
Expected :1
Actual :2
Il motivo per cui comincio da questo primo test potrebbe essere già chiaro ma lo spiego comunque:
voglio essere sicuro che l’ambiente di testing sia configurato correttamente. Ci sono una serie di
cose che potrebbero non funzionare: il phpunit.xml.dist potrebbe essere configurato male, il giusto
eseguibile del PHP potrebbe non essere nel PATH, i path autoload potrebbero non essere configurati
correttamente, potrei essermi dimenticato di scaricare i pacchetti con composer, e chissà cosa altro.
Lanciare il test stupido all’inizio mi permette di verificare velocemente che il framework è
configurato correttamente.
4 Il primo test non stupido
Il primo test non banale che provo è semplicemente questo:
class TripServiceTest extends TestCase
{
function test_something() {
new TripService();
}
}
OK (1 test, 0 assertions)
Process finished with exit code 0
Mi sono limitato a costruire l’oggetto. Anche se non c’è nessuna assert anche questo vale come test:
sto verificando che, in ambiente di test, è possibile costruire un istanza dell’oggetto. Può sembrare
scontato ma non lo è: può succede che con il codice legacy non sia possibile creare direttamente un
oggetto in ambiente di test. Questa volta ci è andata bene perché nel codice dell’esercizio il
costruttore è vuoto, nel legacy reale può succedere di avere dei costruttori con dipendenze implicite
che richiedono precodinzioni particolari per essere eseguito.
5 Proviamo a chiamare il metodo
class TripServiceTest extends TestCase
{
function test_something() {
$service = new TripService();
$service->getTripsByUser(null);
}
}
Il metodo getTripsByUser richiede un parametro, nella maggior parte dei linguaggi mainstream un
modo veloce per verificare se siamo in grado di chiamare il metodo è passare null al posto di ogni
parametro richiesto. Nel nostro caso quando lanciamo i test otteniamo un errore:
TypeError : Argument 1 passed to TripServiceKataTripTripService::getTripsByUser() must be an
instance of TripServiceKataUserUser, null given, called in /Users/andrea/trip-service-
kata/php/tests/TripServiceKata/Trip/TripServiceTest.php on line 11
/Users/andrea/trip-service-kata/php/src/TripServiceKata/Trip/TripService.php:11
/Users/andrea/trip-service-kata/php/tests/TripServiceKata/Trip/TripServiceTest.php:11
Il motivo è che in PHP, nonostante quello che se ne dice, esistono dei timidi controlli statici sui tipi,
infatti se andiamo a vedere la definizione della funzione troviamo che è definita come:
public function getTripsByUser(User $user) {
Il fatto che il tipo della variabile $user sia indicato con la classe User sta a significare che $user non
può essere null. Ok, allora proviamo a modificare il nostro test:
class TripServiceTest extends TestCase
{
function test_something() {
$service = new TripService();
$service->getTripsByUser(new User());
}
}
TripServiceKataExceptionDependentClassCalledDuringUnitTestException :
UserSession.getLoggedUser() should not be called in an unit test
/Users/andrea/trip-service-kata/php/src/TripServiceKata/User/UserSession.php:35
/Users/andrea/trip-service-kata/php/src/TripServiceKata/Trip/TripService.php:13
/Users/andrea/trip-service-kata/php/tests/TripServiceKata/Trip/TripServiceTest.php:12
Questo è il primo errore di un certo peso che riceviamo. Seguiamo lo stack trace partendo dal fondo
per capire cosa sta succedendo.
/Users/andrea/trip-service-kata/php/tests/TripServiceKata/Trip/TripServiceTest.php:12
class TripServiceTest extends TestCase
{
function test_something() {
$service = new TripService();
$service->getTripsByUser(new User(""));
}
}
/Users/andrea/trip-service-kata/php/src/TripServiceKata/Trip/TripService.php:13
class TripService
{
public function getTripsByUser(?User $user) {
$tripList = array();
$loggedUser = UserSession::getInstance()->getLoggedUser();
...
}
}
E poi:
/Users/andrea/trip-service-kata/php/src/TripServiceKata/User/UserSession.php:35
<?php
namespace TripServiceKataUser;
use TripServiceKataExceptionDependentClassCalledDuringUnitTestException;
class UserSession
{
...
public function getLoggedUser()
{
throw new DependentClassCalledDuringUnitTestException(
'UserSession.getLoggedUser() should not be called in an unit
test'
);
}
}
Questo è un esercizio e non è un vero codice legacy. In un vero codice legacy a questo punto il test
dovrebbe aver colpito del codice che fa parte del framework MVC. In questo esercizio non abbiamo
un vero framework MVC proprio perché si tratta di un esercizio e l’autore ha preferito inserire un
abbozzo che ci permetta di svolgere l’esercizio senza dover aggiungere un intero framework MVC.
In questo esercizio quando proviamo ad accedere a qualunque delle funzioni del framework
riceviamo un eccezione. L’eccezione ci informa, correttamente, che non dovremmo chiamare i
metodi del framework dentro i test di unità. Approfondimento: perché non dovremmo voler
chiamare i metodi del framework nei test di unità?
A questo punto però ci troviamo bloccati, non possiamo proseguire nel testing perché la chiamata a
getLoggedUser ci inchioda il test e non ci fa proseguire. Una soluzione potrebbe sembrare il
refactoring: qui ci troviamo di fronte a una dipendenza non iniettata e sembrerebbe proprio il caso
di cambiare la struttura del codice. Però fare refactor su codice non testato è un rischio, chi ci
assicura che faremo un vero refactor, cioè un cambiamento di struttura che non modifica il
comportamento. Approfondimento: Quando mi conviene rischiare?
C’è più di un modo per uscire da questo impasse, qui useremo la caratterizzazione momentanea:
scriviamo un test che caratterizza il comportamento corrente (anche se è una eccezione) e poi, una
volta che il codice è sotto test facciamo il refactor che volevamo fare.
Il comportamento corrente è il lancio di un eccezione, in PHPUnit le eccezioni si verificano usando
$this→expectException():
function test_something() {
$service = new TripService();
$this-
>expectException(DependentClassCalledDuringUnitTestException::class);
$service->getTripsByUser(new User(""));
}
Finalmente il test torna verde:
OK (1 test, 1 assertion)
Facciamo commit e vediamo a che punto siamo arrivati.
• Abbiamo coperto ben tre linee di codice della funzione getTripsByUser().
• Abbiamo un solo test test_something() che forse si potrebbe meritare un nome più
esplicativo.
6 Dove siamo arrivati?
Dove siamo arrivati?
• Abbiamo un test funzionante che copre quasi tre righe del codice di produzione.
Come facciamo ad andare oltre? Il problema qui è che il codice di produzione dipende da un
sistema esterno di cui non abbiamo il controllo, cioè il framework MVC, in questo caso dal
singleton UserSession::GetInstance().
Sandro Mancuso ha rappresentato questa situazione con questo diagramma:
Queste sono le famose dipendenze che andrebbero “rotte” o meglio invertite. A questo punto un
refactor che inverte le dipendenze non si riesce a fare perché richiederebbe di toccare troppi punti
che non sono sotto test. In questi casi si usa il trucco dei Seam. I seam sono un punto del codice di
cui si può cambiare senza modificare quel punto. Ogni seam ha un punto di abilitazione, cioè un
altro punto nel codice dove puoi decidere quale comportamento abilitare o meno.
Di seguito modificheremo leggermente il codice in modo da introdurre un seam, la tecnica di
modifica si chiama “Extract and Override Call” descritta in [Michael C. Feathers. “Working
Effectively with Legacy Code.”] e il tipo di seam che aggiungeremo è un seam di tipo object.
Il primo passo da fare è un “Extract Method”
class TripService
{
public function getTripsByUser(?User $user) {
$tripList = array();
$loggedUser = UserSession::getInstance()->getLoggedUser();
...
}
}
Dopo il refactor avremo:
class TripService
{
public function getTripsByUser(?User $user) {
$tripList = array();
$loggedUser = $this->getLoggedInUser();
...
}
protected function getLoggedInUser()
{
return UserSession::getInstance()->getLoggedUser();
}
}
A questo punto lanciamo i test e vediamo che tutto continua a passare.
PHPUnit 8.0.4 by Sebastian Bergmann and contributors.
Runtime: PHP 7.2.15 with Xdebug 2.6.1
Configuration: /Users/andrea/trip-service-
kata/php/phpunit.xml.dist
Time: 106 ms, Memory: 4.00 MB
OK (1 test, 1 assertion)
7 Box: I refactor più o meno safe degli IDE.
A volte si dice che i refactor degli IDE sarebbero safe, cioè possono essere fatti la necessità di avere
dei test perché l’IDE è in grado di fare le sue verifiche e fare dei refactor fatti bene. La realtà è
diversa, in particolare in questo caso se avvessi lasciato fare il refactor all’IDE avrei ottenuto questo
codice:
class TripService
{
public function getTripsByUser(?User $user) {
$tripList = array();
$loggedUser = $this->getLoggedInUser();
...
}
protected function getLoggedInUser(): void
{
return UserSession::getInstance()->getLoggedUser();
}
}
Il punto di arrivo sembra corretto ma se notate bene il tipo restituito dalla nuova funzione è … void!
Infatti se lancio i test ottengo l’errore:
PHPUnit 8.0.4 by Sebastian Bergmann and contributors.
Runtime: PHP 7.2.15 with Xdebug 2.6.1
Configuration: /Users/andrea/trip-service-kata/php/phpunit.xml.dist
Fatal error: A void function must not return a value in /Users/andrea/trip-
service-kata/php/src/TripServiceKata/Trip/TripService.php on line 33
Call Stack:
0.0015 399664 1. {main}() /Users/andrea/trip-service-
kata/php/vendor/phpunit/phpunit/phpunit:0
0.0352 808704 2. PHPUnitTextUICommand::main() /Users/andrea/trip-
service-kata/php/vendor/phpunit/phpunit/phpunit:61
0.0352 808816 3. PHPUnitTextUICommand->run() /Users/andrea/trip-
service-kata/php/vendor/phpunit/phpunit/src/TextUI/Command.php:164
Per evitare questi problemi la soluzione è lanciare i test di frequente, il più frequente possibile,
possibilmente ad singolo ogni passo di refactoring.
8 Testare la sottoclasse
Testare la sottoclasse (testing subclass) è un pattern che troviamo nel libro di Feathers. L’idea è
usare una sottoclasse con la quale si “silenziano” i comportamenti scomodi: come andare a invocare
dipendenze esterne.
Per prima cosa creiamo una sotto classe:
class TestableTripService extends TripService
{
}
E modifichiamo il test in modo che faccia riferimento a questa classe:
class TripServiceTest extends TestCase
{
function test_something() {
$service = new TestableTripService();
$this-
>expectException(DependentClassCalledDuringUnitTestException::class);
$service->getTripsByUser(new User(""));
}
}
Ci aspettiamo che nulla sia cambiato, lanciamo i test per verificarlo:
PHPUnit 8.0.4 by Sebastian Bergmann and contributors.
Runtime: PHP 7.2.15 with Xdebug 2.6.1
Configuration: /Users/andrea/trip-service-kata/php/phpunit.xml.dist
Time: 158 ms, Memory: 4.00 MB
OK (1 test, 1 assertion)
Ora possiamo silenziare il comportamento scomodo della getLoggedInUser(), per farlo ci basta
fare override del metodo e fornire una implementazione nulla:
class TestableTripService extends TripService
{
protected function getLoggedInUser()
{
return null;
}
}
Ora il comportamento è cambiato. Lancio i test e mi aspetto che in qualche modo lo segnali.
PHPUnit 8.0.4 by Sebastian Bergmann and contributors.
Runtime: PHP 7.2.15 with Xdebug 2.6.1
Configuration: /Users/andrea/trip-service-kata/php/phpunit.xml.dist
Failed asserting that exception of type
"TripServiceKataExceptionUserNotLoggedInException" matches expected exception
"TripServiceKataExceptionDependentClassCalledDuringUnitTestException". Message
was: "" at
/Users/andrea/trip-service-kata/php/src/TripServiceKata/Trip/TripService.php:27
/Users/andrea/trip-service-
kata/php/tests/TripServiceKata/Trip/TripServiceTest.php:22
.
Time: 189 ms, Memory: 6.00 MB
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
Cerchiamo di capire meglio cosa succede, il messaggio di errore contiene già tutte le informazioni
ma noi possiamo agire sul test per rendere più esplicito il motivo per cui fallisce. Sappiamo già che
il comportamento dovrebbe essere cambiato e presumibilmente l’eccezione
DependentClassCalledDuringUnitTestException non viene più lanciata. Modifichiamo velocemente
il test per rendere più chiaro quello che succede.
class TripServiceTest extends TestCase
{
function test_something() {
$service = new TestableTripService();
$service->getTripsByUser(new User(""));
}
}
Per farlo semplicemente cancelliamo il punto dove invochiamo $this->expectException().
Così facendo lasciamo che le eventuali eccezioni generate dal codice sotto test vengano propagate
fino all’esterno.
Ora il messaggio di errore è molto più chiaro:
PHPUnit 8.0.4 by Sebastian Bergmann and contributors.
Runtime: PHP 7.2.15 with Xdebug 2.6.1
Configuration: /Users/andrea/trip-service-kata/php/phpunit.xml.dist
TripServiceKataExceptionUserNotLoggedInException
/Users/andrea/trip-service-kata/php/src/TripServiceKata/Trip/TripService.php:27
/Users/andrea/trip-service-
kata/php/tests/TripServiceKata/Trip/TripServiceTest.php:22
Time: 156 ms, Memory: 6.00 MB
ERRORS!
Tests: 1, Assertions: 0, Errors: 1.
Quello che succede è che viene lanciata l’eccezione UserNotLoggedInException, per passare
velocemente alla barra verde caratterizziamo questo comportamento aggiungendo una nuova
expectException.
function test_something() {
$service = new TestableTripService();
$this->expectException(UserNotLoggedInException::class);
$service->getTripsByUser(new User(""));
}
Vediamo i test passare:
PHPUnit 8.0.4 by Sebastian Bergmann and contributors.
Runtime: PHP 7.2.15 with Xdebug 2.6.1
Configuration: /Users/andrea/trip-service-kata/php/phpunit.xml.dist
Time: 128 ms, Memory: 4.00 MB
OK (1 test, 1 assertion)
Ora che siamo in barra verde possiamo prenderci il tempo per ispezionare lo stato della copertura e
capire cosa è successo.
Come si può vedere dalla figura, ora il test copre qualche riga in più di prima, in tutto sono 6 righe.
Vediamo che ora riesce a superare la riga 13:
Arri
va fino alla riga con l’if e poi salta al ramo else:
Dopo aver letto il comportamento dovrebbe essere chiaro: se l’utente non è loggato il servizio
lancia un’eccezione. Di fatto è proprio quello che verifica il test. Aggiorniamo il nome del test di
conseguenza:
function test_only_logged_user_can_use_the_service() {
$service = new TestableTripService();
$this->expectException(UserNotLoggedInException::class);
$service->getTripsByUser(new User(""));
}
Poi possiamo sistemare un attimo il test aggiungendo il metodo setUp:
class TripServiceTest extends TestCase
{
/** @var TestableTripService */
private $service;
protected function setUp(): void
{
$this->service = new TestableTripService();
}
function test_only_logged_user_can_use_the_service() {
$this->expectException(UserNotLoggedInException::class);
$this->service->getTripsByUser(new User(""));
}
}
9 Testare tutto: un test per ogni ramo
Ogni volta che nel codice troviamo un if, un while o un switch l’esecuzione può prendere rami
differenti. In genere, se vogliamo caratterizzare completamente il comportamento di codice
esistente, potrebbe essere efficace avere che i test coprono tutti le possibili ramificazioni del codice.
Per ottenere questo risultato è comune che venga scritto un test per ogni possibile ramo. Adesso
siamo arrivati a coprire tra i rami quello più esterno (quel rampo di else che lancia l’eccezione).
Adesso andiamo a creare un nuovo test cercando di colpire un ramo differente. Da quale partiamo?
La strategia è quella di cercare il primo bivio che ha beccato il nostro test e fargli prendere una
strada diversa.
Nel nostro caso il bivio è stato preso alla linea 15 dove c’è l’if. Si capisce perché un corpo dell’if è
rimasto rosso e l’altro è verde. La condizione è e in effetti $this-
>getLoggedInUser() restituisce sempre null come configurato nel TestableTripService.
class TestableTripService extends TripService
{
protected function getLoggedInUser()
{
return null;
}
}
L’ideale sarebbe se si potesse pilotare, a seconda del test, che valore deve essere restituito dal
metodo getLoggedInUser(). Vorrei poter scrivere qualcosa del genere:
$this->service->setLoggedInUser("a-new-value");
Scriviamo in un test quello che vorrei poter fare con l’oggetto $this->service:
function test_something() {
$this->service->setLoggedInUser("a-new-value");
}
L’IDE prontamente ci segnala il metodo come non esistente. Sfruttando la segnalazione dell’IDE
faccio creare il metodo:
Aggiungo una assert che richieda anche un effettiva implementazione del metodo:
function test_something() {
$this->service->setLoggedInUser("a-new-value");
self::assertEquals("a-new-value", $this->service->getLoggedInUser());
}
PHPUnit 8.0.4 by Sebastian Bergmann and contributors.
Runtime: PHP 7.2.15 with Xdebug 2.6.1
Configuration: /Users/andrea/trip-service-kata/php/phpunit.xml.dist
Failed asserting that null matches expected 'a-new-value'.
Expected :a-new-value
Actual :null
<Click to see difference>
/Users/andrea/trip-service-
kata/php/tests/TripServiceKata/Trip/TripServiceTest.php:40
Time: 95 ms, Memory: 6.00 MB
FAILURES!
Tests: 2, Assertions: 2, Failures: 1.
F
Scrivo velocemente l’implementazione:
class TestableTripService extends TripService
{
private $loggedInUser;
public function setLoggedInUser($loggedInUser)
{
$this->loggedInUser = $loggedInUser;
}
public function getLoggedInUser()
{
return $this->loggedInUser;
}
}
PHPUnit 8.0.4 by Sebastian Bergmann and contributors.
Runtime: PHP 7.2.15 with Xdebug 2.6.1
Configuration: /Users/andrea/trip-service-kata/php/phpunit.xml.dist
Time: 85 ms, Memory: 4.00 MB
OK (2 tests, 2 assertions)
10 Un nuovo test
Scriviamo il nuovo test che dovrebbe attivare l’altro ramo dell’if:
function test_something() {
$this->service->setLoggedInUser("a-new-value");
$this->service->getTripsByUser(new User(""));
}
E passa:
PHPUnit 8.0.4 by Sebastian Bergmann and contributors.
Runtime: PHP 7.2.15 with Xdebug 2.6.1
Configuration: /Users/andrea/trip-service-kata/php/phpunit.xml.dist
Time: 80 ms, Memory: 4.00 MB
OK (2 tests, 1 assertion)
Process finished with exit code 0
Se il test passa senza lanciare eccezioni vuol dire che questa volta l’esecuzione ha toccato un return
della funzione. Prima di procedere oltre fissiamo un po’ di più questo test, aggiungiamo un
asserzione sul valore di ritorno:
function test_something() {
$this->service->setLoggedInUser("a-new-value");
$trips_found = $this->service->getTripsByUser(new User(""));
self::assertEquals("", $trips_found);
}
Notate che ho scritto asserzione senza cercare di capire prima quale fosse il valore giusto, anzi ho
scelto apposta un valore che sono quasi sicuro sia sbagliato: la stringa vuota “” per valore che
dovrebbe essere un array. Ho scelto un valore sbagliato perché in questo modo sfrutto il test in
modo che mi dica lui quale è il valore giusto: con il suo messaggio di fallimento:
PHPUnit 8.0.4 by Sebastian Bergmann and contributors.
Runtime: PHP 7.2.15 with Xdebug 2.6.1
Configuration: /Users/andrea/trip-service-kata/php/phpunit.xml.dist
Array () does not match expected type "string".
/Users/andrea/trip-service-
kata/php/tests/TripServiceKata/Trip/TripServiceTest.php:46
Time: 121 ms, Memory: 6.00 MB
FAILURES!
Tests: 2, Assertions: 2, Failures: 1.
Mi dice che l’array non matcha con la stringa. Ora so cosa scrivere per fare passare il test:
function test_something() {
$this->service->setLoggedInUser("a-new-value");
$trips_found = $this->service->getTripsByUser(new User(""));
self::assertEquals([], $trips_found);
}
Ora il test passa:
PHPUnit 8.0.4 by Sebastian Bergmann and contributors.
Runtime: PHP 7.2.15 with Xdebug 2.6.1
Configuration: /Users/andrea/trip-service-kata/php/phpunit.xml.dist
Time: 89 ms, Memory: 4.00 MB
OK (2 tests, 2 assertions)
Process finished with exit code 0
Diamo un’occhiata alla copertura del codice:
Adesso ci piacerebbe entrare nel prossimo ramo. Il contenuto del foreach non viene toccato,
probabilmente perché $user->getFriends() restituisce un array vuoto. Diamo un occhiata alla classe
User:
class User
{
private $trips;
private $friends;
private $name;
public function __construct($name)
{
$this->name = $name;
$this->trips = array();
$this->friends = array();
}
public function getTrips()
{
return $this->trips;
}
public function getFriends()
{
return $this->friends;
}
public function addFriend(User $user)
{
$this->friends[] = $user;
}
public function addTrip(Trip $trip)
{
$this->trips[] = $trip;
}
}
È una classe molto semplice, il metodo getFriends() restituisce un array di amici dell’utente, e il
metodo addFriend() popola la lista. Modifichiamo il test in modo che l’utente spiato abbia degli
amici;
function test_something() {
$this->service->setLoggedInUser("a-new-value");
$utente_spiato = new User("");
$utente_spiato->addFriend(new User("un amico"));
$utente_spiato->addFriend(new User("un altro amico"));
$trips_found = $this->service->getTripsByUser($utente_spiato);
self::assertEquals([], $trips_found);
}
Lanciamo i test e sono verdi. Controlliamo la copertura:
Siamo riusciti ad entrare nel foreach. Non siamo riusciti ad entrare dentro l’if.
Per entrare dentro l’if uno degli amici deve essere l’utente loggato. Creiamo un secondo test
copiando e incollando quello esistente e modifichiamolo:
function test_something_being_a_friend() {
$loggedInUser = new User("a-new-value");
$this->service->setLoggedInUser($loggedInUser);
$utente_spiato = new User("");
$utente_spiato->addFriend($loggedInUser);
$trips_found = $this->service->getTripsByUser($utente_spiato);
self::assertEquals([], $trips_found);
}
Vediamo il risultato dei test: tutti passano meno uno:
PHPUnit 8.0.4 by Sebastian Bergmann and contributors.
Runtime: PHP 7.2.15 with Xdebug 2.6.1
Configuration: /Users/andrea/trip-service-kata/php/phpunit.xml.dist
TripServiceKataExceptionDependentClassCalledDuringUnitTestException : TripDAO
should not be invoked on an unit test.
/Users/andrea/trip-service-kata/php/src/TripServiceKata/Trip/TripDAO.php:12
/Users/andrea/trip-service-kata/php/src/TripServiceKata/Trip/TripService.php:23
/Users/andrea/trip-service-
kata/php/tests/TripServiceKata/Trip/TripServiceTest.php:58
Time: 151 ms, Memory: 6.00 MB
ERRORS!
Tests: 3, Assertions: 2, Errors: 1.
Process finished with exit code 2
Cerchiamo di capire cosa è successo guardando allo stack trace.
La primo elemento dello stack vediamo che l’esecuzione che ha portato all’eccezione è partita dalla
chiamata al nostro servizio nel nuovo test.
/Users/andrea/trip-service-kata/php/tests/TripServiceKata/Trip/TripServiceTest.php:58
/Users/andrea/trip-service-kata/php/src/TripServiceKata/Trip/TripService.php:23
Dal secondo elemento dello stack trace vediamo che il nostro test ha attivato finalmente un pezzo di
codice che non era stato ancora coperto.
Dal terzo elemento vediamo che siamo arrivati a toccare il DAO, l’oggetto che in teoria dovrebbe
accedere al database.
Come prima, trattandosi di un esercizio, non è un vero DAO, è una versione abbozzata di un
ipotetico DAO che invece di accedere al database lancia una eccezione.
Come prima dobbiamo rompere questa dipendenza, come prima, prima di modificare il codice
cerchiamo di coprirlo con un test che passa. Come prima usiamo expectException per caratterizzare
il comportamento.
function test_something_being_a_friend() {
$loggedInUser = new User("a-new-value");
$this->service->setLoggedInUser($loggedInUser);
$utente_spiato = new User("");
$utente_spiato→addFriend($loggedInUser);
$this→expectException(
DependentClassCalledDuringUnitTestException::class);
$trips_found = $this->service->getTripsByUser($utente_spiato);
self::assertEquals([], $trips_found);
}
Se lanciamo i test: tutto passa.
Come prima dobbiamo cerchiamo di rompere la dipendeza con il pattern “Extract and Override
Call”.
Nel codice di seguito ho evidenziato il pezzo di codice da estrarre:
public function getTripsByUser(User $user) {
$tripList = array();
$loggedUser = $this->getLoggedInUser();
$isFriend = false;
if ($loggedUser != null) {
foreach ($user->getFriends() as $friend) {
if ($friend == $loggedUser) {
$isFriend = true;
break;
}
}
if ($isFriend) {
$tripList = TripDAO::findTripsByUser($user);
}
return $tripList;
} else {
throw new UserNotLoggedInException();
}
}
Lo estraggo:
public function getTripsByUser(User $user) {
$tripList = array();
$loggedUser = $this->getLoggedInUser();
$isFriend = false;
if ($loggedUser != null) {
foreach ($user->getFriends() as $friend) {
if ($friend == $loggedUser) {
$isFriend = true;
break;
}
}
if ($isFriend) {
$tripList = $this->loadTripsOfUser($user);
}
return $tripList;
} else {
throw new UserNotLoggedInException();
}
}
protected function loadTripsOfUser(User $user)
{
return TripDAO::findTripsByUser($user);
}
Ora possiamo agire sul TestableTripService per cambiare il comportamento:
class TestableTripService extends TripService
{
...
protected function loadTripsOfUser(User $user)
{
return "trips of user";
}
}
Vediamo cosa succede a lanciare i test:
PHPUnit 8.0.4 by Sebastian Bergmann and contributors.
Runtime: PHP 7.2.15 with Xdebug 2.6.1
Configuration: /Users/andrea/trip-service-kata/php/phpunit.xml.dist
'trips of user' does not match expected type "array".
/Users/andrea/trip-service-kata/php/tests/TripServiceKata/Trip/TripServiceTest.php:68
Time: 172 ms, Memory: 6.00 MB
FAILURES!
Tests: 3, Assertions: 3, Failures: 1.
Come al solito la assertEquals ci dice quale dovrebbe essere il giusto valore. Modifichiamo il test
inserendo il giusto valore ('trips of user') e lanciando i test.
function test_something_being_a_friend() {
$loggedInUser = new User("a-new-value");
$this->service->setLoggedInUser($loggedInUser);
$utente_spiato = new User("");
$utente_spiato->addFriend($loggedInUser);
$this-
>expectException(DependentClassCalledDuringUnitTestException::class);
$trips_found = $this->service->getTripsByUser($utente_spiato);
self::assertEquals('trips of user', $trips_found);
}
Dopo aver lanciato i test otteniamo:
PHPUnit 8.0.4 by Sebastian Bergmann and contributors.
Runtime: PHP 7.2.15 with Xdebug 2.6.1
Configuration: /Users/andrea/trip-service-kata/php/phpunit.xml.dist
Failed asserting that exception of type
"TripServiceKataExceptionDependentClassCalledDuringUnitTestException" is
thrown.
Time: 114 ms, Memory: 6.00 MB
FAILURES!
Tests: 3, Assertions: 4, Failures: 1.
I test ci stanno dicendo che ci siamo dimenticati un expectException che non serve più.
function test_something_being_a_friend() {
$loggedInUser = new User("a-new-value");
$this->service->setLoggedInUser($loggedInUser);
$utente_spiato = new User("");
$utente_spiato->addFriend($loggedInUser);
$this-
>expectException(DependentClassCalledDuringUnitTestException::class);
$trips_found = $this->service->getTripsByUser($utente_spiato);
self::assertEquals('trips of user', $trips_found);
}
Rilanciamo i test e tutto passa.
Diamo un occhiata ai nostri test:
class TripServiceTest extends TestCase
{
/** @var TestableTripService */
private $service;
protected function setUp(): void
{
$this->service = new TestableTripService();
}
function test_only_logged_user_can_use_the_service() {
$this->expectException(UserNotLoggedInException::class);
$this->service->getTripsByUser(new User(""));
}
function test_something() {
$this->service->setLoggedInUser("a-new-value");
$utente_spiato = new User("");
$utente_spiato->addFriend(new User("un amico"));
$utente_spiato->addFriend(new User("un altro amico"));
$trips_found = $this->service->getTripsByUser($utente_spiato);
self::assertEquals([], $trips_found);
}
function test_something_being_a_friend() {
$loggedInUser = new User("a-new-value");
$this->service->setLoggedInUser($loggedInUser);
$utente_spiato = new User("");
$utente_spiato->addFriend($loggedInUser);
$trips_found = $this->service->getTripsByUser($utente_spiato);
self::assertEquals('trips of user', $trips_found);
}
}
Stiamo coprendo questi casi:
• utente non loggato
• utente loggato che cerca di vedere i contenuti di un utente non amico (di uno sconosciuto)
• utente loggato che cerca di vedere i contenuti di un utente amico
Diamo un occhiata alla copertura del codice.
I test coprono completamente il metodo getTripsByUser().
I test lasciano fuori la parte che si interfaccia con i servizi esterni.
Le due funzioni loadTripsOfUser() e getLoggedUser() sono dei boilerplate che abbiamo usato per
disaccoppiarci dalle due dipendenze del servizio TripService.
class TripServiceTest extends TestCase
{
/** @var TestableTripService */
private $service;
protected function setUp(): void
{
$this->service = new TestableTripService();
}
function test_only_logged_user_can_use_the_service() {
$this->expectException(UserNotLoggedInException::class);
$this->service->getTripsByUser(new User(""));
}
function test_something() {
$this->service->setLoggedInUser("a-new-value");
$utente_spiato = new User("");
$utente_spiato->addFriend(new User("un amico"));
$utente_spiato->addFriend(new User("un altro amico"));
$trips_found = $this->service->getTripsByUser($utente_spiato);
self::assertEquals([], $trips_found);
}
function test_something_being_a_friend() {
$loggedInUser = new User("a-new-value");
$this->service->setLoggedInUser($loggedInUser);
$utente_spiato = new User("");
$utente_spiato->addFriend($loggedInUser);
$trips_found = $this->service->getTripsByUser($utente_spiato);
self::assertEquals('trips of user', $trips_found);
}
}
11 Adesso refactor!
Adesso che il codice è coperto da test possiamo fare anche fare tutto il refactor che vogliamo. Da
dove partiamo?
Rifattorizzare anche i test
I programmatori che usano il test automatico alla fine si trovano con la codebase idealmente (e
spesso praticamente) divisa in due parti:
• il codice di produzione e
• il codice di test.
Quale delle due parti è più importante? Quale parte del codice dobbiamo curare di più? Ho
incontrato spesso team che capivano (almeno in teoria) l’importanza di avere del codice di
produzione pulito ma che non considerava il codice di test altrettanto importante. Questi team di
solito se fanno refactor lo fanno solo sul codice di produzione e lasciano indietro le possibili
migliorie ai test.
La mia esperienza è che i test non vanno considerati come codice di serie B, piuttosto conviene
rifattorizzare tutto il codice.
Consideriamo il codice di test creato durante questo esercizio. Adesso abbiamo definito tre test:
• test_only_logged_user_can_use_the_service()
• test_something()
• test_something_being_a_friend()
I nomi dei test sono un po’ criptici, un semplice primo refactor che faccio è quello di rinominare i
casi di test:
• test_only_logged_user_can_use_the_service()
• test_logged_user_can_not_see_trips_of_a_stranger()
• test_logged_user_can_see_trips_of_a_friend()
Ora ci possiamo concentrare su ognuno dei singoli metodi di test partendo dal primo:
function test_logged_user_can_not_see_trips_of_a_stranger() {
$this->service->setLoggedInUser("a-new-value");
$utente_spiato = new User("");
$utente_spiato->addFriend(new User("un amico"));
$utente_spiato->addFriend(new User("un altro amico"));
$trips_found = $this->service->getTripsByUser($utente_spiato);
self::assertEquals([], $trips_found);
}
La prima modifica è organizzare il codice di test all’interno del metodo usando il pattern AAA
(Arrange Act Assert). L’idea alla base del pattern AAA è che la maggior parte dei test può essere
pensato in tre fasi:
• Arrange: preparo l’oggetto sotto test e i vari collaboratori per l’esecuzione
• Act: invoco il metodo sotto test.
• Assert: verifico che il risultato dell’azione si quello che ci si aspettava
Nel nostro caso il codice può essere raggruppato in questo modo:
function test_logged_user_can_not_see_trips_of_a_stranger() {
// Arrange
$this->service->setLoggedInUser("a-new-value");
$utente_spiato = new User("");
$utente_spiato->addFriend(new User("un amico"));
$utente_spiato->addFriend(new User("un altro amico"));
// Act
$trips_found = $this→service→getTripsByUser($utente_spiato);
// Assert
self::assertEquals([], $trips_found);
}
Nella parte di “Arrange” ci sono queste ben tre linee spese per affermare un solo concetto: l’utente
di cui vengono spiati i viaggi in questo test è uno sconosciuto.
$utente_spiato = new User("");
$utente_spiato->addFriend(new User("un amico"));
$utente_spiato->addFriend(new User("un altro amico"));
Un modo più semplice per esprime questo concetto è usare una factory:
function test_logged_user_can_not_see_trips_of_a_stranger() {
// Arrange
$this->service->setLoggedInUser("a-new-value");
$utente_spiato = $this->unoSconosciuto();
// Act
$trips_found = $this->service->getTripsByUser($utente_spiato);
// Assert
self::assertEquals([], $trips_found);
}
private function unoSconosciuto() {
$sconosciuto = new User("");
$sconosciuto->addFriend(new User("un amico"));
$sconosciuto->addFriend(new User("un altro amico"));
return $sconosciuto;
}
Volendo si può anche mettere inline:
function test_logged_user_can_not_see_trips_of_a_stranger() {
// Arrange
$this->service->setLoggedInUser("a-new-value");
// Act
$trips_found = $this->service->getTripsByUser($this->unoSconosciuto());
// Assert
self::assertEquals([], $trips_found);
}
A questo punto è giunto il momento di svelarvi un segreto riguardo al pattern Arrange Act Assert:
dato che le tre fasi si ripetono praticamente sempre uguali nei test in genere si evita di mettere i
commenti e le tre fasi si evidenziano semplicemente lasciando una riga vuota tra una fase e l’altra:
function test_logged_user_can_not_see_trips_of_a_stranger() {
$this->service->setLoggedInUser("a-new-value");
$trips_found = $this->service->getTripsByUser($this->unoSconosciuto());
self::assertEquals([], $trips_found);
}
Un ultima cosa che mi fa storcere il naso è la stringa che rappresenta l’utente loggato, possiamo
usare un oggetto User e usare un campo dell’oggetto:
/** @var User */
private $a_registered_user;
protected function setUp(): void
{
...
$this->a_registered_user = new User("a registered user");
}
...
function test_logged_user_can_not_see_trips_of_a_stranger() {
$this->service->setLoggedInUser($this->a_registered_user);
$trips_found = $this->service->getTripsByUser($this->unoSconosciuto());
self::assertEquals([], $trips_found);
}
12 Refactor del secondo test
Adesso il test appare così:
function test_logged_user_can_see_trips_of_a_friend() {
$loggedInUser = new User("a-new-value");
$this->service->setLoggedInUser($loggedInUser);
$utente_spiato = new User("");
$utente_spiato->addFriend($loggedInUser);
$trips_found = $this->service->getTripsByUser($utente_spiato);
self::assertEquals('trips of user', $trips_found);
}
Il pezzo di codice in mezzo:
$utente_spiato = new User("");
$utente_spiato->addFriend($loggedInUser);
serve per creare un utente amico dell’utente loggato. Rendiamo evidente questa intenzione. Per
farlo potremmo usare un commento:
function test_logged_user_can_see_trips_of_a_friend() {
$loggedInUser = new User("a-new-value");
$this->service->setLoggedInUser($loggedInUser);
// crea un amico dell'utente loggato
$utente_spiato = new User("");
$utente_spiato->addFriend($loggedInUser);
$trips_found = $this->service->getTripsByUser($utente_spiato);
self::assertEquals('trips of user', $trips_found);
}
In realtà un commento si può quasi sempre trasformare nel nome di un metodo:
function test_logged_user_can_see_trips_of_a_friend() {
$loggedInUser = new User("a-new-value");
$this->service->setLoggedInUser($loggedInUser);
$utente_spiato = $this->unAmicoDi($loggedInUser);
$trips_found = $this->service->getTripsByUser($utente_spiato);
self::assertEquals('trips of user', $trips_found);
}
private function unAmicoDi(User $loggedInUser)
{
$utente_amico = new User("");
$utente_amico->addFriend($loggedInUser);
return $utente_amico;
}
Poi la prima parte in realtà può essere rivista usando la proprietà $a_registered_user creata prima:
function test_logged_user_can_see_trips_of_a_friend() {
$this->service->setLoggedInUser($this->a_registered_user);
$utente_spiato = $this->unAmicoDi($this->a_registered_user);
$trips_found = $this->service->getTripsByUser($utente_spiato);
self::assertEquals('trips of user', $trips_found);
}
Vediamo l’ultimo test:
function test_only_logged_user_can_use_the_service() {
$this->expectException(UserNotLoggedInException::class);
$this->service->getTripsByUser(new User(""));
}
In questo test si dice che ci si aspetta una eccezione UserNotLoggedInException però nel testo
del test per me non è abbastanza evidente quando questo succede. Quello che sappiamo è che
l’eccezione avviene: 1) quando l’utente non è loggato e 2) qualunque sia l’utente spiato.
Modifichiamo il test in modo che queste due informazioni siano più chiare:
function test_only_logged_user_can_use_the_service() {
$this->nessunUtenteLoggato();
$utente_spiato = $this->unUtenteQualunque();
$this->expectException(UserNotLoggedInException::class);
$this->service->getTripsByUser($utente_spiato);
}
private function nessunUtenteLoggato(): void
{
return $this->service->setLoggedInUser(null);
}
private function unUtenteQualunque()
{
return new User("uno qualunque");
}
Arrange-Act-Assert oppure Arrange-Expect-Act? Notate che questo test non è organizzato
secondo Arrange-Act-Assert proprio perché, in questo caso, non c’è una asserzione ma un
aspettativa(expect). La suddivisioni in fasi Arrange-Expect-Act è tipica dei test i framework di
mock basati sulla definizione di aspettative (expectations).
13 Refactor del codice di produzione
Ora passiamo al refactor del codice di produzione.
class TripService
{
public function getTripsByUser(User $user) {
$tripList = array();
$loggedUser = $this->getLoggedInUser();
$isFriend = false;
if ($loggedUser != null) {
foreach ($user->getFriends() as $friend) {
if ($friend == $loggedUser) {
$isFriend = true;
break;
}
}
if ($isFriend) {
$tripList = $this->loadTripsOfUser($user);
}
return $tripList;
} else {
throw new UserNotLoggedInException();
}
}
protected function loadTripsOfUser(User $user)
{
return TripDAO::findTripsByUser($user);
}
protected function getLoggedInUser()
{
return UserSession::getInstance()->getLoggedUser();
}
}
Ci sono due grandi aspetti di questo codice che chiedono di essere sistemati:
• il corpo del metodo principale è molto complesso
• le dipendenze non solo non sono iniettate ma dipendono direttamente da dettagli
implementativi (come lo specifico framework MVC, e la specifica classe che si interfaccia
al database).
Mi concentro per primo sul design del metodo:
public function getTripsByUser(User $user) {
$tripList = array();
$loggedUser = $this->getLoggedInUser();
$isFriend = false;
if ($loggedUser != null) {
foreach ($user->getFriends() as $friend) {
if ($friend == $loggedUser) {
$isFriend = true;
break;
}
}
if ($isFriend) {
$tripList = $this->loadTripsOfUser($user);
}
return $tripList;
} else {
throw new UserNotLoggedInException();
}
}
Comincio con la prima linea:
$tripList = array();
Qui inzializzaziamo una variable che viene usata solo molto dopo nel codice. La sposto:
public function getTripsByUser(User $user) {
$loggedUser = $this->getLoggedInUser();
$isFriend = false;
if ($loggedUser != null) {
foreach ($user->getFriends() as $friend) {
if ($friend == $loggedUser) {
$isFriend = true;
break;
}
}
$tripList = array();
if ($isFriend) {
$tripList = $this->loadTripsOfUser($user);
}
return $tripList;
} else {
throw new UserNotLoggedInException();
}
}
Mi concentro su questo blocco:
$tripList = array();
if ($isFriend) {
$tripList = $this->loadTripsOfUser($user);
}
return $tripList;
Potrebbe essere espresso senza usare la variabile temporanea:
if ($isFriend) {
return $this->loadTripsOfUser($user);
}
return array();
E poi la riga:
return array();
Si può esprimere usando il literal per gli array vuoti:
Torniamo a dare uno sguardo di insieme al metodo:
public function getTripsByUser(User $user) {
$loggedUser = $this->getLoggedInUser();
$isFriend = false;
if ($loggedUser != null) {
foreach ($user->getFriends() as $friend) {
if ($friend == $loggedUser) {
$isFriend = true;
break;
}
}
if ($isFriend) {
return $this->loadTripsOfUser($user);
}
return [];
} else {
throw new UserNotLoggedInException();
}
}
Anche variabile $isFriend è inizializzata molto prima di essere usata, possiamo spostarla giù.
public function getTripsByUser(User $user) {
$loggedUser = $this->getLoggedInUser();
if ($loggedUser != null) {
$isFriend = false;
foreach ($user->getFriends() as $friend) {
if ($friend == $loggedUser) {
$isFriend = true;
break;
}
}
if ($isFriend) {
return $this->loadTripsOfUser($user);
}
return [];
} else {
throw new UserNotLoggedInException();
}
}
Mi concentro su blocco:
$isFriend = false;
foreach ($user->getFriends() as $friend) {
if ($friend == $loggedUser) {
$isFriend = true;
break;
}
}
E capisco che in realtà il suo unico scopo è calcolare unico boolean che mi dice se $loggedUser e
amico di $user. Aggiungo prima un commento:
// determine if $user is friend of $loggedUser
$isFriend = false;
foreach ($user->getFriends() as $friend) {
if ($friend == $loggedUser) {
$isFriend = true;
break;
}
}
Poi lo trasformo in una chiamata a metodo. È una cosa incrementale prima aggiungo una chiamata
ad un metodo fantasma:
$isFriend = $user->isFriendOf($loggedUser);
// determine if $user is friend of $loggedUser
$isFriend = false;
foreach ($user->getFriends() as $friend) {
if ($friend == $loggedUser) {
$isFriend = true;
break;
}
}
Se lancio i test giustamente falliscono perché User# isFriendOf non esiste.
Error : Call to undefined method TripServiceKataUserUser::isFriendOf()
/Users/andrea/trip-service-kata/php/src/TripServiceKata/Trip/TripService.php:15
/Users/andrea/trip-service-kata/php/tests/TripServiceKata/Trip/TripServiceTest.php:66
Faccio passare i test creando il metodo vuoto:
...
class User
{
...
function isFriendOf($loggedUser)
{
}
}
Poi gli copio dentro il contenuto del blocco:
function isFriendOf($loggedUser)
{
// determine if $user is friend of $loggedUser
$isFriend = false;
foreach ($user->getFriends() as $friend) {
if ($friend == $loggedUser) {
$isFriend = true;
break;
}
}
}
Prima di lanciare i test sistemo un po’:
function isFriendOf($loggedUser)
{
// determine if $user is friend of $loggedUser
$isFriend = false;
foreach ($this->getFriends() as $friend) {
if ($friend == $loggedUser) {
$isFriend = true;
break;
}
}
return $isFriend;
}
14 Invertire gli if
Siamo arrivati a questa situazione:
public function getTripsByUser(User $user) {
$loggedUser = $this->getLoggedInUser();
if ($loggedUser != null) {
$isFriend = $user->isFriendOf($loggedUser);
if ($isFriend) {
return $this->loadTripsOfUser($user);
}
return [];
} else {
throw new UserNotLoggedInException();
}
}
L’if più esterno può essere invertito e trasformato in una guardia:
public function getTripsByUser(User $user)
{
$loggedUser = $this->getLoggedInUser();
if ($loggedUser == null) throw new UserNotLoggedInException();
$isFriend = $user->isFriendOf($loggedUser);
if ($isFriend) {
return $this->loadTripsOfUser($user);
}
return [];
}
Della variabile $isFriend si può fare l’inline.
public function getTripsByUser(User $user)
{
$loggedUser = $this->getLoggedInUser();
if ($loggedUser == null) throw new UserNotLoggedInException();
if ($user->isFriendOf($loggedUser)) {
return $this->loadTripsOfUser($user);
}
return [];
}
Anche il secondo if si può invertire:
public function getTripsByUser(User $user)
{
$loggedUser = $this->getLoggedInUser();
if ($loggedUser == null) throw new UserNotLoggedInException();
if (!$user->isFriendOf($loggedUser)) return [];
return $this->loadTripsOfUser($user);
}
Alla fine abbiamo due guardie a cui segue l’esecuzione dell’happy path.
15 Iniettare le dipendenze
Il codice è questo:
public function getTripsByUser(User $user)
{
$loggedUser = $this->getLoggedInUser();
if ($loggedUser == null) throw new UserNotLoggedInException();
if (!$user->isFriendOf($loggedUser)) return [];
return $this->loadTripsOfUser($user);
}
...
protected function getLoggedInUser()
{
return UserSession::getInstance()->getLoggedUser();
}
Una che stona un po’ è il fatto che il servizio per funzionare avrebbe bisogno del valore
$loggedUser e per recuperarlo fa due lookup (uno per accedere singleton
UserSession::getInstance() e un altro per accedere al getLoggedUser()) . Io in questi casi
preferisco usare l’iniezione delle dipendenze rispetto al lookup. Sarebbe tutto più semplice se la
signature del metodo getTripsByUser() prevedesse già il parametro $loggedUser. Qualcosa
del tipo:
public function getTripsByUser(User $user, User? $loggedUser) ...
Questo però sarebbe un cambiamento all’interfaccia del metodo, si può fare ma bisogna tenere
conto che i test che abbiamo adesso coprono solo il singolo oggetto TripService ma non abbiamo
nessun test che verifica come le altre classi si integrano con TripService.
Questo refactor è grandino e si può splittare in step:
• Aggiunta del parametro $loggedUser a getTripsByUser() (senza ancora usarlo)
• Switch dall’uso attuale (lookup) all’uso nuovo (parametro)
• Pulizia delle parti non più necessarie.
16 Step 1/3: Aggiunta del parametro senza ancora
usarlo
Prima di tutto aggiungiamo il parametro:
public function getTripsByUser(User $user, $loggedUser)
Se lanciamo i test adesso non passeranno ma il compilatore ci farà il piacere di dirci tutti i punti
dove bisogna aggiungere il parametro:
Testing started at 15:42 ...
/usr/local/Cellar/php@7.2/7.2.15/bin/php /Users/andrea/trip-service-
kata/php/vendor/phpunit/phpunit/phpunit --configuration /Users/andrea/trip-
service-kata/php/phpunit.xml.dist TripServiceKataTripTripServiceTest
/Users/andrea/trip-service-
kata/php/tests/TripServiceKata/Trip/TripServiceTest.php --teamcity
PHPUnit 8.0.4 by Sebastian Bergmann and contributors.
Runtime: PHP 7.2.15 with Xdebug 2.6.1
Configuration: /Users/andrea/trip-service-kata/php/phpunit.xml.dist
ArgumentCountError : Too few arguments to function
TripServiceKataTripTripService::getTripsByUser(), 1 passed in
/Users/andrea/trip-service-
kata/php/tests/TripServiceKata/Trip/TripServiceTest.php on line 66 and exactly 2
expected
/Users/andrea/trip-service-kata/php/src/TripServiceKata/Trip/TripService.php:11
/Users/andrea/trip-service-
kata/php/tests/TripServiceKata/Trip/TripServiceTest.php:66
ArgumentCountError : Too few arguments to function
TripServiceKataTripTripService::getTripsByUser(), 1 passed in
/Users/andrea/trip-service-
kata/php/tests/TripServiceKata/Trip/TripServiceTest.php on line 75 and exactly 2
expected
/Users/andrea/trip-service-kata/php/src/TripServiceKata/Trip/TripService.php:11
/Users/andrea/trip-service-
kata/php/tests/TripServiceKata/Trip/TripServiceTest.php:75
Failed asserting that exception of type "ArgumentCountError" matches expected
exception "TripServiceKataExceptionUserNotLoggedInException". Message was:
"Too few arguments to function
TripServiceKataTripTripService::getTripsByUser(), 1 passed in
/Users/andrea/trip-service-
kata/php/tests/TripServiceKata/Trip/TripServiceTest.php on line 49 and exactly 2
expected" at
/Users/andrea/trip-service-kata/php/src/TripServiceKata/Trip/TripService.php:11
/Users/andrea/trip-service-
kata/php/tests/TripServiceKata/Trip/TripServiceTest.php:49
.
Time: 99 ms, Memory: 6.00 MB
ERRORS!
Tests: 3, Assertions: 1, Errors: 2, Failures: 1.
Process finished with exit code 2
Sistemo gli errori del compilatore uno alla volta (questa tecnica si dice “Lean on compiler”):
Primo punto:
function test_only_logged_user_can_use_the_service() {
$this->nessunUtenteLoggato();
$utente_spiato = $this->unUtenteQualunque();
$this->expectException(UserNotLoggedInException::class);
$this->service->getTripsByUser($utente_spiato, null);
}
Secondo punto:
function test_logged_user_can_not_see_trips_of_a_stranger() {
$this->service->setLoggedInUser($this->a_registered_user);
$utente_spiato = $this->unoSconosciuto();
$trips_found = $this->service->getTripsByUser($utente_spiato, $this-
>a_registered_user);
self::assertEquals([], $trips_found);
}
Terzo punto:
function test_logged_user_can_see_trips_of_a_friend() {
$this->service->setLoggedInUser($this->a_registered_user);
$utente_spiato = $this->unAmicoDi($this->a_registered_user);
$trips_found = $this->service->getTripsByUser($utente_spiato, $this-
>a_registered_user);
self::assertEquals('trips of user', $trips_found);
}
Ora se lancio i test passano tutti.
17 Step 2/3: Switch alla nuova versione
Il codice del servizio adesso è:
public function getTripsByUser(User $user, $loggedUser)
{
$loggedUser = $this->getLoggedInUser();
if ($loggedUser == null) throw new UserNotLoggedInException();
if (!$user->isFriendOf($loggedUser)) return [];
return $this->loadTripsOfUser($user);
}
Al metodo arriva il parametro ma non viene ancora usato, anzi si usa il metodo vecchio. Per passare
al nuovo modo mi basta cancellare la riga che chima getLoggedInUser().
public function getTripsByUser(User $user, $loggedUser)
{
$loggedUser = $this->getLoggedInUser();
if ($loggedUser == null) throw new UserNotLoggedInException();
if (!$user->isFriendOf($loggedUser)) return [];
return $this->loadTripsOfUser($user);
}
Lancio i test e tutti passano.
18 Step 3/3: pulizia del modo vecchio
A questo punto abbiamo tutti i test che passano ma ci è rimasto del codice che non serve più. Passo
a fare pulizia e ad ogni passo lancio i test.
Cancello il metodo getLoggedInUser() da TripService:
class TripService
{
public function getTripsByUser(User $user, $loggedUser)
{
if ($loggedUser == null) throw new UserNotLoggedInException();
if (!$user->isFriendOf($loggedUser)) return [];
return $this->loadTripsOfUser($user);
}
protected function loadTripsOfUser(User $user)
{
...
}
protected function getLoggedInUser()
{
return UserSession::getInstance()->getLoggedUser();
}
}
Lo cancello da TestableTripService:
class TestableTripService extends TripService
{
private $loggedInUser;
public function setLoggedInUser($loggedInUser)
{
$this->loggedInUser = $loggedInUser;
}
public function getLoggedInUser()
{
return $this->loggedInUser;
}
protected function loadTripsOfUser(User $user)
{
return "trips of user";
}
}
Lancio i test e tutti passano.
A questo punto non è neanche più necessario il metodo setLoggedInUser(). Prima però devo
cancellare tutte le sue chiamate.
function test_only_logged_user_can_use_the_service() {
$this->nessunUtenteLoggato();
$utente_spiato = $this->unUtenteQualunque();
$this->expectException(UserNotLoggedInException::class);
$this->service->getTripsByUser($utente_spiato, null);
}
private function nessunUtenteLoggato()
{
$this->service->setLoggedInUser(null);
}
private function unUtenteQualunque()
{
return new User("uno qualunque");
}
function test_logged_user_can_not_see_trips_of_a_stranger() {
$this->service->setLoggedInUser($this->a_registered_user);
$utente_spiato = $this->unoSconosciuto();
$trips_found = $this->service->getTripsByUser($utente_spiato, $this-
>a_registered_user);
self::assertEquals([], $trips_found);
}
function test_logged_user_can_see_trips_of_a_friend() {
$this->service->setLoggedInUser($this->a_registered_user);
$utente_spiato = $this->unAmicoDi($this->a_registered_user);
$trips_found = $this->service->getTripsByUser($utente_spiato, $this-
>a_registered_user);
self::assertEquals('trips of user', $trips_found);
}
Mi accorgo anche che adesso il metodo nessunUtenteLoggato() risulta vuoto, quindi posso
buttare via lui e l’unica sua chiamata rimasta.
function test_only_logged_user_can_use_the_service() {
$this->nessunUtenteLoggato();
$utente_spiato = $this->unUtenteQualunque();
$this->expectException(UserNotLoggedInException::class);
$this->service->getTripsByUser($utente_spiato, null);
}
private function nessunUtenteLoggato()
{
}
Ora che ho rimosso tutte le chiamate posso tornare su TestableTripService e buttare via
setLoggedInUser().
class TestableTripService extends TripService
{
private $loggedInUser;
public function setLoggedInUser($loggedInUser)
{
$this->loggedInUser = $loggedInUser;
}
protected function loadTripsOfUser(User $user)
{
return "trips of user";
}
}
Lancio i test e tutti passano.
19 Iniettare la dipendenza per l’accesso al DB
TBD

Contenu connexe

Tendances

TDD in WordPress
TDD in WordPressTDD in WordPress
TDD in WordPresslucatume
 
Introduzione al Test Driven Development
Introduzione al Test Driven DevelopmentIntroduzione al Test Driven Development
Introduzione al Test Driven DevelopmentEnnio Masi
 
Tdd.Every.Where.21012012
Tdd.Every.Where.21012012Tdd.Every.Where.21012012
Tdd.Every.Where.21012012LEGALDESK
 
PowerMock TDD User Group Milano
PowerMock TDD User Group MilanoPowerMock TDD User Group Milano
PowerMock TDD User Group MilanoMassimo Groppelli
 
Design pattern template method
Design pattern template methodDesign pattern template method
Design pattern template methodNelson Firmani
 
Test double - un'introduzione (PHP)
Test double - un'introduzione (PHP)Test double - un'introduzione (PHP)
Test double - un'introduzione (PHP)Carmelantonio Zolfo
 
Programmazione a oggetti tramite la macchina del caffé (pt. 2)
Programmazione a oggetti tramite la macchina del caffé (pt. 2)Programmazione a oggetti tramite la macchina del caffé (pt. 2)
Programmazione a oggetti tramite la macchina del caffé (pt. 2)Marcello Missiroli
 
Programmazione a oggetti tramite la macchina del caffé (pt. 3)
Programmazione a oggetti tramite la macchina del caffé (pt. 3)Programmazione a oggetti tramite la macchina del caffé (pt. 3)
Programmazione a oggetti tramite la macchina del caffé (pt. 3)Marcello Missiroli
 
Unit Test di Gabriele Seroni
Unit Test di Gabriele SeroniUnit Test di Gabriele Seroni
Unit Test di Gabriele SeroniGiuneco S.r.l
 
Programmazione a oggetti tramite la macchina del caffé (1/3)
Programmazione a oggetti tramite la macchina del caffé (1/3)Programmazione a oggetti tramite la macchina del caffé (1/3)
Programmazione a oggetti tramite la macchina del caffé (1/3)Marcello Missiroli
 

Tendances (15)

TDD in WordPress
TDD in WordPressTDD in WordPress
TDD in WordPress
 
Introduzione al Test Driven Development
Introduzione al Test Driven DevelopmentIntroduzione al Test Driven Development
Introduzione al Test Driven Development
 
Tdd.Every.Where.21012012
Tdd.Every.Where.21012012Tdd.Every.Where.21012012
Tdd.Every.Where.21012012
 
Testing
TestingTesting
Testing
 
PowerMock TDD User Group Milano
PowerMock TDD User Group MilanoPowerMock TDD User Group Milano
PowerMock TDD User Group Milano
 
Design pattern template method
Design pattern template methodDesign pattern template method
Design pattern template method
 
Test double - un'introduzione (PHP)
Test double - un'introduzione (PHP)Test double - un'introduzione (PHP)
Test double - un'introduzione (PHP)
 
Java OCA teoria 5
Java OCA teoria 5Java OCA teoria 5
Java OCA teoria 5
 
Programmazione a oggetti tramite la macchina del caffé (pt. 2)
Programmazione a oggetti tramite la macchina del caffé (pt. 2)Programmazione a oggetti tramite la macchina del caffé (pt. 2)
Programmazione a oggetti tramite la macchina del caffé (pt. 2)
 
Programmazione a oggetti tramite la macchina del caffé (pt. 3)
Programmazione a oggetti tramite la macchina del caffé (pt. 3)Programmazione a oggetti tramite la macchina del caffé (pt. 3)
Programmazione a oggetti tramite la macchina del caffé (pt. 3)
 
Il testing con zend framework
Il testing con zend frameworkIl testing con zend framework
Il testing con zend framework
 
Unit Test di Gabriele Seroni
Unit Test di Gabriele SeroniUnit Test di Gabriele Seroni
Unit Test di Gabriele Seroni
 
Programmazione a oggetti tramite la macchina del caffé (1/3)
Programmazione a oggetti tramite la macchina del caffé (1/3)Programmazione a oggetti tramite la macchina del caffé (1/3)
Programmazione a oggetti tramite la macchina del caffé (1/3)
 
Unit Testing
Unit TestingUnit Testing
Unit Testing
 
PhpUnit
PhpUnitPhpUnit
PhpUnit
 

Similaire à Baby Steps TripServiceKata

Presentazione Testing automatizzato
Presentazione Testing automatizzatoPresentazione Testing automatizzato
Presentazione Testing automatizzatoangelolu
 
Dominare il codice legacy
Dominare il codice legacyDominare il codice legacy
Dominare il codice legacyTommaso Torti
 
Javaday 2006: Java 5
Javaday 2006: Java 5Javaday 2006: Java 5
Javaday 2006: Java 5Matteo Baccan
 
Software Testing & Test Driven Development
Software Testing & Test Driven DevelopmentSoftware Testing & Test Driven Development
Software Testing & Test Driven DevelopmentSergio Santoro
 
Laboratorio Programmazione: Funzioni
Laboratorio Programmazione: FunzioniLaboratorio Programmazione: Funzioni
Laboratorio Programmazione: FunzioniMajong DevJfu
 
Tdd e continuous delivery sull'infrastruttura
Tdd e continuous delivery sull'infrastrutturaTdd e continuous delivery sull'infrastruttura
Tdd e continuous delivery sull'infrastrutturaCodemotion
 
Mocking Objects Practices
Mocking Objects PracticesMocking Objects Practices
Mocking Objects PracticesGrUSP
 
Working between the clouds (versione completa)
Working between the clouds (versione completa)Working between the clouds (versione completa)
Working between the clouds (versione completa)Davide Cerbo
 
The Hitchhiker's Guide to testable code: semplici regole per scrivere codice ...
The Hitchhiker's Guide to testable code: semplici regole per scrivere codice ...The Hitchhiker's Guide to testable code: semplici regole per scrivere codice ...
The Hitchhiker's Guide to testable code: semplici regole per scrivere codice ...Davide Cerbo
 
TDD e Continuous Delivery sull'infrastruttura
TDD e Continuous Delivery sull'infrastrutturaTDD e Continuous Delivery sull'infrastruttura
TDD e Continuous Delivery sull'infrastrutturaFilippo Liverani
 
Googletest, tdd e mock
Googletest, tdd e mockGoogletest, tdd e mock
Googletest, tdd e mockyuroller
 
Android Test Driven Development
Android Test Driven DevelopmentAndroid Test Driven Development
Android Test Driven Developmentsazilla
 
Android Test Driven Development
Android Test Driven DevelopmentAndroid Test Driven Development
Android Test Driven Developmentsazilla
 
Rich client application: MVC4 + MVVM = Knockout.js
Rich client application: MVC4 + MVVM = Knockout.jsRich client application: MVC4 + MVVM = Knockout.js
Rich client application: MVC4 + MVVM = Knockout.jsGiorgio Di Nardo
 
Testing in javascript
Testing in javascriptTesting in javascript
Testing in javascriptPiero Bozzolo
 

Similaire à Baby Steps TripServiceKata (20)

Java codestyle & tipstricks
Java codestyle & tipstricksJava codestyle & tipstricks
Java codestyle & tipstricks
 
Concurrency
ConcurrencyConcurrency
Concurrency
 
Presentazione Testing automatizzato
Presentazione Testing automatizzatoPresentazione Testing automatizzato
Presentazione Testing automatizzato
 
Dominare il codice legacy
Dominare il codice legacyDominare il codice legacy
Dominare il codice legacy
 
Javaday 2006: Java 5
Javaday 2006: Java 5Javaday 2006: Java 5
Javaday 2006: Java 5
 
Software Testing & Test Driven Development
Software Testing & Test Driven DevelopmentSoftware Testing & Test Driven Development
Software Testing & Test Driven Development
 
Workshop: Introduzione ad TDD
Workshop: Introduzione ad TDDWorkshop: Introduzione ad TDD
Workshop: Introduzione ad TDD
 
Software Testing e TDD
Software Testing e TDDSoftware Testing e TDD
Software Testing e TDD
 
Laboratorio Programmazione: Funzioni
Laboratorio Programmazione: FunzioniLaboratorio Programmazione: Funzioni
Laboratorio Programmazione: Funzioni
 
Tdd e continuous delivery sull'infrastruttura
Tdd e continuous delivery sull'infrastrutturaTdd e continuous delivery sull'infrastruttura
Tdd e continuous delivery sull'infrastruttura
 
Mocking Objects Practices
Mocking Objects PracticesMocking Objects Practices
Mocking Objects Practices
 
Working between the clouds (versione completa)
Working between the clouds (versione completa)Working between the clouds (versione completa)
Working between the clouds (versione completa)
 
The Hitchhiker's Guide to testable code: semplici regole per scrivere codice ...
The Hitchhiker's Guide to testable code: semplici regole per scrivere codice ...The Hitchhiker's Guide to testable code: semplici regole per scrivere codice ...
The Hitchhiker's Guide to testable code: semplici regole per scrivere codice ...
 
TDD e Continuous Delivery sull'infrastruttura
TDD e Continuous Delivery sull'infrastrutturaTDD e Continuous Delivery sull'infrastruttura
TDD e Continuous Delivery sull'infrastruttura
 
Googletest, tdd e mock
Googletest, tdd e mockGoogletest, tdd e mock
Googletest, tdd e mock
 
Android Test Driven Development
Android Test Driven DevelopmentAndroid Test Driven Development
Android Test Driven Development
 
Android Test Driven Development
Android Test Driven DevelopmentAndroid Test Driven Development
Android Test Driven Development
 
Il testing con zend framework
Il testing con zend frameworkIl testing con zend framework
Il testing con zend framework
 
Rich client application: MVC4 + MVVM = Knockout.js
Rich client application: MVC4 + MVVM = Knockout.jsRich client application: MVC4 + MVVM = Knockout.js
Rich client application: MVC4 + MVVM = Knockout.js
 
Testing in javascript
Testing in javascriptTesting in javascript
Testing in javascript
 

Plus de Andrea Francia

TDD on Legacy Code - Voxxed Days Milano 2019
TDD on Legacy Code - Voxxed Days Milano 2019TDD on Legacy Code - Voxxed Days Milano 2019
TDD on Legacy Code - Voxxed Days Milano 2019Andrea Francia
 
Lavorare con codice legacy “non testabile” - Incontro DevOps - 8 marzo 2019 -...
Lavorare con codice legacy “non testabile” - Incontro DevOps - 8 marzo 2019 -...Lavorare con codice legacy “non testabile” - Incontro DevOps - 8 marzo 2019 -...
Lavorare con codice legacy “non testabile” - Incontro DevOps - 8 marzo 2019 -...Andrea Francia
 
Kata in Bash a DevOpsHeroes 2018 a Parma
Kata in Bash a DevOpsHeroes 2018 a ParmaKata in Bash a DevOpsHeroes 2018 a Parma
Kata in Bash a DevOpsHeroes 2018 a ParmaAndrea Francia
 
User Stories - Andrea Francia @ WeDev 7 novembre 2018
User Stories - Andrea Francia @ WeDev 7 novembre 2018User Stories - Andrea Francia @ WeDev 7 novembre 2018
User Stories - Andrea Francia @ WeDev 7 novembre 2018Andrea Francia
 
Le pratiche ingegneristiche di eXtreme Programming
Le pratiche ingegneristiche di eXtreme ProgrammingLe pratiche ingegneristiche di eXtreme Programming
Le pratiche ingegneristiche di eXtreme ProgrammingAndrea Francia
 
Test-Driven Development su Codice Esistente
Test-Driven Development su Codice EsistenteTest-Driven Development su Codice Esistente
Test-Driven Development su Codice EsistenteAndrea Francia
 
Le 12 pratiche - Un introduzione a XP (Mini Italian Agile Day)
Le 12 pratiche - Un introduzione a XP (Mini Italian Agile Day)Le 12 pratiche - Un introduzione a XP (Mini Italian Agile Day)
Le 12 pratiche - Un introduzione a XP (Mini Italian Agile Day)Andrea Francia
 
Introduzione a eXtreme Programming
Introduzione a eXtreme ProgrammingIntroduzione a eXtreme Programming
Introduzione a eXtreme ProgrammingAndrea Francia
 
Test-Driven Development e Sviluppo Incrementale (TDD-Milano 2017-01-10)
Test-Driven Development e Sviluppo Incrementale (TDD-Milano 2017-01-10)Test-Driven Development e Sviluppo Incrementale (TDD-Milano 2017-01-10)
Test-Driven Development e Sviluppo Incrementale (TDD-Milano 2017-01-10)Andrea Francia
 
Piccolo coding dojo (milano xpug 2013-04-11)
Piccolo coding dojo (milano xpug 2013-04-11)Piccolo coding dojo (milano xpug 2013-04-11)
Piccolo coding dojo (milano xpug 2013-04-11)Andrea Francia
 
Tutti i miei sbagli (Errori di un wannabe Open Source Developer)
Tutti i miei sbagli (Errori di un wannabe Open Source Developer)Tutti i miei sbagli (Errori di un wannabe Open Source Developer)
Tutti i miei sbagli (Errori di un wannabe Open Source Developer)Andrea Francia
 
Tutti i miei sbagli, versione 7 Marzo 2012 al XPUG mi
Tutti i miei sbagli, versione 7 Marzo 2012 al XPUG miTutti i miei sbagli, versione 7 Marzo 2012 al XPUG mi
Tutti i miei sbagli, versione 7 Marzo 2012 al XPUG miAndrea Francia
 
Writing a Crawler with Python and TDD
Writing a Crawler with Python and TDDWriting a Crawler with Python and TDD
Writing a Crawler with Python and TDDAndrea Francia
 
Google C++ Testing Framework in Visual Studio 2008
Google C++ Testing Framework in Visual Studio 2008Google C++ Testing Framework in Visual Studio 2008
Google C++ Testing Framework in Visual Studio 2008Andrea Francia
 
Subversion @ JUG Milano 11 dic 2009
Subversion @ JUG Milano 11 dic 2009Subversion @ JUG Milano 11 dic 2009
Subversion @ JUG Milano 11 dic 2009Andrea Francia
 

Plus de Andrea Francia (20)

TDD on Legacy Code - Voxxed Days Milano 2019
TDD on Legacy Code - Voxxed Days Milano 2019TDD on Legacy Code - Voxxed Days Milano 2019
TDD on Legacy Code - Voxxed Days Milano 2019
 
Lavorare con codice legacy “non testabile” - Incontro DevOps - 8 marzo 2019 -...
Lavorare con codice legacy “non testabile” - Incontro DevOps - 8 marzo 2019 -...Lavorare con codice legacy “non testabile” - Incontro DevOps - 8 marzo 2019 -...
Lavorare con codice legacy “non testabile” - Incontro DevOps - 8 marzo 2019 -...
 
Kata in Bash a DevOpsHeroes 2018 a Parma
Kata in Bash a DevOpsHeroes 2018 a ParmaKata in Bash a DevOpsHeroes 2018 a Parma
Kata in Bash a DevOpsHeroes 2018 a Parma
 
User Stories - Andrea Francia @ WeDev 7 novembre 2018
User Stories - Andrea Francia @ WeDev 7 novembre 2018User Stories - Andrea Francia @ WeDev 7 novembre 2018
User Stories - Andrea Francia @ WeDev 7 novembre 2018
 
Le pratiche ingegneristiche di eXtreme Programming
Le pratiche ingegneristiche di eXtreme ProgrammingLe pratiche ingegneristiche di eXtreme Programming
Le pratiche ingegneristiche di eXtreme Programming
 
Test-Driven Development su Codice Esistente
Test-Driven Development su Codice EsistenteTest-Driven Development su Codice Esistente
Test-Driven Development su Codice Esistente
 
Come si applica l'OCP
Come si applica l'OCPCome si applica l'OCP
Come si applica l'OCP
 
Le 12 pratiche - Un introduzione a XP (Mini Italian Agile Day)
Le 12 pratiche - Un introduzione a XP (Mini Italian Agile Day)Le 12 pratiche - Un introduzione a XP (Mini Italian Agile Day)
Le 12 pratiche - Un introduzione a XP (Mini Italian Agile Day)
 
Introduzione a eXtreme Programming
Introduzione a eXtreme ProgrammingIntroduzione a eXtreme Programming
Introduzione a eXtreme Programming
 
Test-Driven Development e Sviluppo Incrementale (TDD-Milano 2017-01-10)
Test-Driven Development e Sviluppo Incrementale (TDD-Milano 2017-01-10)Test-Driven Development e Sviluppo Incrementale (TDD-Milano 2017-01-10)
Test-Driven Development e Sviluppo Incrementale (TDD-Milano 2017-01-10)
 
Le 12 pratiche
Le 12 praticheLe 12 pratiche
Le 12 pratiche
 
Bash-Only Deployment
Bash-Only DeploymentBash-Only Deployment
Bash-Only Deployment
 
TDD anche su iOS
TDD anche su iOSTDD anche su iOS
TDD anche su iOS
 
Piccolo coding dojo (milano xpug 2013-04-11)
Piccolo coding dojo (milano xpug 2013-04-11)Piccolo coding dojo (milano xpug 2013-04-11)
Piccolo coding dojo (milano xpug 2013-04-11)
 
Tutti i miei sbagli (Errori di un wannabe Open Source Developer)
Tutti i miei sbagli (Errori di un wannabe Open Source Developer)Tutti i miei sbagli (Errori di un wannabe Open Source Developer)
Tutti i miei sbagli (Errori di un wannabe Open Source Developer)
 
Tutti i miei sbagli, versione 7 Marzo 2012 al XPUG mi
Tutti i miei sbagli, versione 7 Marzo 2012 al XPUG miTutti i miei sbagli, versione 7 Marzo 2012 al XPUG mi
Tutti i miei sbagli, versione 7 Marzo 2012 al XPUG mi
 
Writing a Crawler with Python and TDD
Writing a Crawler with Python and TDDWriting a Crawler with Python and TDD
Writing a Crawler with Python and TDD
 
Introduzione al TDD
Introduzione al TDDIntroduzione al TDD
Introduzione al TDD
 
Google C++ Testing Framework in Visual Studio 2008
Google C++ Testing Framework in Visual Studio 2008Google C++ Testing Framework in Visual Studio 2008
Google C++ Testing Framework in Visual Studio 2008
 
Subversion @ JUG Milano 11 dic 2009
Subversion @ JUG Milano 11 dic 2009Subversion @ JUG Milano 11 dic 2009
Subversion @ JUG Milano 11 dic 2009
 

Baby Steps TripServiceKata

  • 1. 1 L’esercizio L’esercizio che risolvo in queste pagine è il TripServiceKata di Sandro Mancuso (https://github.com/sandromancuso/trip-service-kata). In questo esercizio si prende del codice legacy esistente, lo si mette sotto test e lo si rifattorizza per migliorarne il design. L’esercizio è già stato risolto più volte in pubblico dallo stesso Mancuso e in rete si trovano i filmati dove Mancuso mostra la sua soluzione. Quella che riporto è la mia soluzione, riprende molto dell’originale ma ci sono anche delle differenze. La prima differenza è il linguaggio di programmazione, l’originale di Mancuso usava il Java io uso il PHP, questo mi permette di mostrare un paio di cose che in Java non si possono usare. Se volete vedere la soluzione originale di Mancuso potete andare a farlo su YouTube: https://www.youtube.com/watch?v=_NnElPO5BU0 Se volete vedere come la faccio io continuare di seguito. 2 Il codice legacy da testare L’obiettivo dell’esercizio è riuscire a testare il codice legacy mostrato sotto. <?php namespace TripServiceKataTrip; use TripServiceKataUserUser; use TripServiceKataUserUserSession; use TripServiceKataExceptionUserNotLoggedInException; class TripService { public function getTripsByUser(User $user) { $tripList = array(); $loggedUser = UserSession::getInstance()->getLoggedUser(); $isFriend = false; if ($loggedUser != null) { foreach ($user->getFriends() as $friend) { if ($friend == $loggedUser) { $isFriend = true; break; } } if ($isFriend) { $tripList = TripDAO::findTripsByUser($user); } return $tripList; } else { throw new UserNotLoggedInException(); } } }
  • 2. È una classe che implementa un servizio di un Social Network per viaggiatori. In sintesi i requisiti di questo servizio sono i seguenti: • solo gli utenti loggati possono accedere al servizio • il servizio permette di vedere i viaggi degli altri utenti • si possono vedere i viaggi dei soli utenti “amici”, i viaggi degli sconosciuti non si possono vedere 3 Testare il framework di testing Il primo test che lancio è sempre il test stupido. Cioè un test che sono sicuro fallisca e che fallisca con un messaggio che conosco bene. class TripServiceTest extends TestCase { function test_something() { self::assertEquals(1,2); } } Controllo che 1 è uguale a 2. Sono sicuro che fallisca, e, se tutto è configurato bene, dovrebbe darmi un messaggio come quello di seguito: Failed asserting that 2 matches expected 1. Expected :1 Actual :2 Il motivo per cui comincio da questo primo test potrebbe essere già chiaro ma lo spiego comunque: voglio essere sicuro che l’ambiente di testing sia configurato correttamente. Ci sono una serie di cose che potrebbero non funzionare: il phpunit.xml.dist potrebbe essere configurato male, il giusto eseguibile del PHP potrebbe non essere nel PATH, i path autoload potrebbero non essere configurati correttamente, potrei essermi dimenticato di scaricare i pacchetti con composer, e chissà cosa altro. Lanciare il test stupido all’inizio mi permette di verificare velocemente che il framework è configurato correttamente. 4 Il primo test non stupido Il primo test non banale che provo è semplicemente questo: class TripServiceTest extends TestCase { function test_something() { new TripService(); } } OK (1 test, 0 assertions) Process finished with exit code 0 Mi sono limitato a costruire l’oggetto. Anche se non c’è nessuna assert anche questo vale come test: sto verificando che, in ambiente di test, è possibile costruire un istanza dell’oggetto. Può sembrare scontato ma non lo è: può succede che con il codice legacy non sia possibile creare direttamente un
  • 3. oggetto in ambiente di test. Questa volta ci è andata bene perché nel codice dell’esercizio il costruttore è vuoto, nel legacy reale può succedere di avere dei costruttori con dipendenze implicite che richiedono precodinzioni particolari per essere eseguito. 5 Proviamo a chiamare il metodo class TripServiceTest extends TestCase { function test_something() { $service = new TripService(); $service->getTripsByUser(null); } } Il metodo getTripsByUser richiede un parametro, nella maggior parte dei linguaggi mainstream un modo veloce per verificare se siamo in grado di chiamare il metodo è passare null al posto di ogni parametro richiesto. Nel nostro caso quando lanciamo i test otteniamo un errore: TypeError : Argument 1 passed to TripServiceKataTripTripService::getTripsByUser() must be an instance of TripServiceKataUserUser, null given, called in /Users/andrea/trip-service- kata/php/tests/TripServiceKata/Trip/TripServiceTest.php on line 11 /Users/andrea/trip-service-kata/php/src/TripServiceKata/Trip/TripService.php:11 /Users/andrea/trip-service-kata/php/tests/TripServiceKata/Trip/TripServiceTest.php:11 Il motivo è che in PHP, nonostante quello che se ne dice, esistono dei timidi controlli statici sui tipi, infatti se andiamo a vedere la definizione della funzione troviamo che è definita come: public function getTripsByUser(User $user) { Il fatto che il tipo della variabile $user sia indicato con la classe User sta a significare che $user non può essere null. Ok, allora proviamo a modificare il nostro test: class TripServiceTest extends TestCase { function test_something() { $service = new TripService(); $service->getTripsByUser(new User()); } } TripServiceKataExceptionDependentClassCalledDuringUnitTestException : UserSession.getLoggedUser() should not be called in an unit test /Users/andrea/trip-service-kata/php/src/TripServiceKata/User/UserSession.php:35 /Users/andrea/trip-service-kata/php/src/TripServiceKata/Trip/TripService.php:13 /Users/andrea/trip-service-kata/php/tests/TripServiceKata/Trip/TripServiceTest.php:12 Questo è il primo errore di un certo peso che riceviamo. Seguiamo lo stack trace partendo dal fondo per capire cosa sta succedendo.
  • 4. /Users/andrea/trip-service-kata/php/tests/TripServiceKata/Trip/TripServiceTest.php:12 class TripServiceTest extends TestCase { function test_something() { $service = new TripService(); $service->getTripsByUser(new User("")); } } /Users/andrea/trip-service-kata/php/src/TripServiceKata/Trip/TripService.php:13 class TripService { public function getTripsByUser(?User $user) { $tripList = array(); $loggedUser = UserSession::getInstance()->getLoggedUser(); ... } } E poi: /Users/andrea/trip-service-kata/php/src/TripServiceKata/User/UserSession.php:35 <?php namespace TripServiceKataUser; use TripServiceKataExceptionDependentClassCalledDuringUnitTestException; class UserSession { ... public function getLoggedUser() { throw new DependentClassCalledDuringUnitTestException( 'UserSession.getLoggedUser() should not be called in an unit test' ); } } Questo è un esercizio e non è un vero codice legacy. In un vero codice legacy a questo punto il test dovrebbe aver colpito del codice che fa parte del framework MVC. In questo esercizio non abbiamo un vero framework MVC proprio perché si tratta di un esercizio e l’autore ha preferito inserire un abbozzo che ci permetta di svolgere l’esercizio senza dover aggiungere un intero framework MVC. In questo esercizio quando proviamo ad accedere a qualunque delle funzioni del framework riceviamo un eccezione. L’eccezione ci informa, correttamente, che non dovremmo chiamare i metodi del framework dentro i test di unità. Approfondimento: perché non dovremmo voler chiamare i metodi del framework nei test di unità? A questo punto però ci troviamo bloccati, non possiamo proseguire nel testing perché la chiamata a getLoggedUser ci inchioda il test e non ci fa proseguire. Una soluzione potrebbe sembrare il refactoring: qui ci troviamo di fronte a una dipendenza non iniettata e sembrerebbe proprio il caso di cambiare la struttura del codice. Però fare refactor su codice non testato è un rischio, chi ci assicura che faremo un vero refactor, cioè un cambiamento di struttura che non modifica il
  • 5. comportamento. Approfondimento: Quando mi conviene rischiare? C’è più di un modo per uscire da questo impasse, qui useremo la caratterizzazione momentanea: scriviamo un test che caratterizza il comportamento corrente (anche se è una eccezione) e poi, una volta che il codice è sotto test facciamo il refactor che volevamo fare. Il comportamento corrente è il lancio di un eccezione, in PHPUnit le eccezioni si verificano usando $this→expectException(): function test_something() { $service = new TripService(); $this- >expectException(DependentClassCalledDuringUnitTestException::class); $service->getTripsByUser(new User("")); } Finalmente il test torna verde: OK (1 test, 1 assertion) Facciamo commit e vediamo a che punto siamo arrivati. • Abbiamo coperto ben tre linee di codice della funzione getTripsByUser(). • Abbiamo un solo test test_something() che forse si potrebbe meritare un nome più esplicativo. 6 Dove siamo arrivati? Dove siamo arrivati? • Abbiamo un test funzionante che copre quasi tre righe del codice di produzione.
  • 6. Come facciamo ad andare oltre? Il problema qui è che il codice di produzione dipende da un sistema esterno di cui non abbiamo il controllo, cioè il framework MVC, in questo caso dal singleton UserSession::GetInstance(). Sandro Mancuso ha rappresentato questa situazione con questo diagramma: Queste sono le famose dipendenze che andrebbero “rotte” o meglio invertite. A questo punto un refactor che inverte le dipendenze non si riesce a fare perché richiederebbe di toccare troppi punti che non sono sotto test. In questi casi si usa il trucco dei Seam. I seam sono un punto del codice di cui si può cambiare senza modificare quel punto. Ogni seam ha un punto di abilitazione, cioè un altro punto nel codice dove puoi decidere quale comportamento abilitare o meno. Di seguito modificheremo leggermente il codice in modo da introdurre un seam, la tecnica di modifica si chiama “Extract and Override Call” descritta in [Michael C. Feathers. “Working Effectively with Legacy Code.”] e il tipo di seam che aggiungeremo è un seam di tipo object. Il primo passo da fare è un “Extract Method” class TripService { public function getTripsByUser(?User $user) { $tripList = array(); $loggedUser = UserSession::getInstance()->getLoggedUser(); ... } } Dopo il refactor avremo: class TripService { public function getTripsByUser(?User $user) { $tripList = array(); $loggedUser = $this->getLoggedInUser(); ... } protected function getLoggedInUser() { return UserSession::getInstance()->getLoggedUser(); } }
  • 7. A questo punto lanciamo i test e vediamo che tutto continua a passare. PHPUnit 8.0.4 by Sebastian Bergmann and contributors. Runtime: PHP 7.2.15 with Xdebug 2.6.1 Configuration: /Users/andrea/trip-service- kata/php/phpunit.xml.dist Time: 106 ms, Memory: 4.00 MB OK (1 test, 1 assertion) 7 Box: I refactor più o meno safe degli IDE. A volte si dice che i refactor degli IDE sarebbero safe, cioè possono essere fatti la necessità di avere dei test perché l’IDE è in grado di fare le sue verifiche e fare dei refactor fatti bene. La realtà è diversa, in particolare in questo caso se avvessi lasciato fare il refactor all’IDE avrei ottenuto questo codice: class TripService { public function getTripsByUser(?User $user) { $tripList = array(); $loggedUser = $this->getLoggedInUser(); ... } protected function getLoggedInUser(): void { return UserSession::getInstance()->getLoggedUser(); } } Il punto di arrivo sembra corretto ma se notate bene il tipo restituito dalla nuova funzione è … void! Infatti se lancio i test ottengo l’errore: PHPUnit 8.0.4 by Sebastian Bergmann and contributors. Runtime: PHP 7.2.15 with Xdebug 2.6.1 Configuration: /Users/andrea/trip-service-kata/php/phpunit.xml.dist Fatal error: A void function must not return a value in /Users/andrea/trip- service-kata/php/src/TripServiceKata/Trip/TripService.php on line 33 Call Stack: 0.0015 399664 1. {main}() /Users/andrea/trip-service- kata/php/vendor/phpunit/phpunit/phpunit:0 0.0352 808704 2. PHPUnitTextUICommand::main() /Users/andrea/trip- service-kata/php/vendor/phpunit/phpunit/phpunit:61 0.0352 808816 3. PHPUnitTextUICommand->run() /Users/andrea/trip- service-kata/php/vendor/phpunit/phpunit/src/TextUI/Command.php:164 Per evitare questi problemi la soluzione è lanciare i test di frequente, il più frequente possibile,
  • 8. possibilmente ad singolo ogni passo di refactoring. 8 Testare la sottoclasse Testare la sottoclasse (testing subclass) è un pattern che troviamo nel libro di Feathers. L’idea è usare una sottoclasse con la quale si “silenziano” i comportamenti scomodi: come andare a invocare dipendenze esterne. Per prima cosa creiamo una sotto classe: class TestableTripService extends TripService { } E modifichiamo il test in modo che faccia riferimento a questa classe: class TripServiceTest extends TestCase { function test_something() { $service = new TestableTripService(); $this- >expectException(DependentClassCalledDuringUnitTestException::class); $service->getTripsByUser(new User("")); } } Ci aspettiamo che nulla sia cambiato, lanciamo i test per verificarlo: PHPUnit 8.0.4 by Sebastian Bergmann and contributors. Runtime: PHP 7.2.15 with Xdebug 2.6.1 Configuration: /Users/andrea/trip-service-kata/php/phpunit.xml.dist Time: 158 ms, Memory: 4.00 MB OK (1 test, 1 assertion) Ora possiamo silenziare il comportamento scomodo della getLoggedInUser(), per farlo ci basta fare override del metodo e fornire una implementazione nulla: class TestableTripService extends TripService { protected function getLoggedInUser() { return null; } } Ora il comportamento è cambiato. Lancio i test e mi aspetto che in qualche modo lo segnali. PHPUnit 8.0.4 by Sebastian Bergmann and contributors.
  • 9. Runtime: PHP 7.2.15 with Xdebug 2.6.1 Configuration: /Users/andrea/trip-service-kata/php/phpunit.xml.dist Failed asserting that exception of type "TripServiceKataExceptionUserNotLoggedInException" matches expected exception "TripServiceKataExceptionDependentClassCalledDuringUnitTestException". Message was: "" at /Users/andrea/trip-service-kata/php/src/TripServiceKata/Trip/TripService.php:27 /Users/andrea/trip-service- kata/php/tests/TripServiceKata/Trip/TripServiceTest.php:22 . Time: 189 ms, Memory: 6.00 MB FAILURES! Tests: 1, Assertions: 1, Failures: 1. Cerchiamo di capire meglio cosa succede, il messaggio di errore contiene già tutte le informazioni ma noi possiamo agire sul test per rendere più esplicito il motivo per cui fallisce. Sappiamo già che il comportamento dovrebbe essere cambiato e presumibilmente l’eccezione DependentClassCalledDuringUnitTestException non viene più lanciata. Modifichiamo velocemente il test per rendere più chiaro quello che succede. class TripServiceTest extends TestCase { function test_something() { $service = new TestableTripService(); $service->getTripsByUser(new User("")); } } Per farlo semplicemente cancelliamo il punto dove invochiamo $this->expectException(). Così facendo lasciamo che le eventuali eccezioni generate dal codice sotto test vengano propagate fino all’esterno. Ora il messaggio di errore è molto più chiaro: PHPUnit 8.0.4 by Sebastian Bergmann and contributors. Runtime: PHP 7.2.15 with Xdebug 2.6.1 Configuration: /Users/andrea/trip-service-kata/php/phpunit.xml.dist TripServiceKataExceptionUserNotLoggedInException /Users/andrea/trip-service-kata/php/src/TripServiceKata/Trip/TripService.php:27 /Users/andrea/trip-service- kata/php/tests/TripServiceKata/Trip/TripServiceTest.php:22 Time: 156 ms, Memory: 6.00 MB ERRORS!
  • 10. Tests: 1, Assertions: 0, Errors: 1. Quello che succede è che viene lanciata l’eccezione UserNotLoggedInException, per passare velocemente alla barra verde caratterizziamo questo comportamento aggiungendo una nuova expectException. function test_something() { $service = new TestableTripService(); $this->expectException(UserNotLoggedInException::class); $service->getTripsByUser(new User("")); } Vediamo i test passare: PHPUnit 8.0.4 by Sebastian Bergmann and contributors. Runtime: PHP 7.2.15 with Xdebug 2.6.1 Configuration: /Users/andrea/trip-service-kata/php/phpunit.xml.dist Time: 128 ms, Memory: 4.00 MB OK (1 test, 1 assertion) Ora che siamo in barra verde possiamo prenderci il tempo per ispezionare lo stato della copertura e capire cosa è successo. Come si può vedere dalla figura, ora il test copre qualche riga in più di prima, in tutto sono 6 righe. Vediamo che ora riesce a superare la riga 13:
  • 11. Arri va fino alla riga con l’if e poi salta al ramo else: Dopo aver letto il comportamento dovrebbe essere chiaro: se l’utente non è loggato il servizio lancia un’eccezione. Di fatto è proprio quello che verifica il test. Aggiorniamo il nome del test di conseguenza: function test_only_logged_user_can_use_the_service() { $service = new TestableTripService(); $this->expectException(UserNotLoggedInException::class); $service->getTripsByUser(new User("")); } Poi possiamo sistemare un attimo il test aggiungendo il metodo setUp: class TripServiceTest extends TestCase { /** @var TestableTripService */ private $service; protected function setUp(): void { $this->service = new TestableTripService(); } function test_only_logged_user_can_use_the_service() { $this->expectException(UserNotLoggedInException::class); $this->service->getTripsByUser(new User("")); } } 9 Testare tutto: un test per ogni ramo Ogni volta che nel codice troviamo un if, un while o un switch l’esecuzione può prendere rami differenti. In genere, se vogliamo caratterizzare completamente il comportamento di codice esistente, potrebbe essere efficace avere che i test coprono tutti le possibili ramificazioni del codice. Per ottenere questo risultato è comune che venga scritto un test per ogni possibile ramo. Adesso siamo arrivati a coprire tra i rami quello più esterno (quel rampo di else che lancia l’eccezione). Adesso andiamo a creare un nuovo test cercando di colpire un ramo differente. Da quale partiamo? La strategia è quella di cercare il primo bivio che ha beccato il nostro test e fargli prendere una strada diversa.
  • 12. Nel nostro caso il bivio è stato preso alla linea 15 dove c’è l’if. Si capisce perché un corpo dell’if è rimasto rosso e l’altro è verde. La condizione è e in effetti $this- >getLoggedInUser() restituisce sempre null come configurato nel TestableTripService. class TestableTripService extends TripService { protected function getLoggedInUser() { return null; } } L’ideale sarebbe se si potesse pilotare, a seconda del test, che valore deve essere restituito dal metodo getLoggedInUser(). Vorrei poter scrivere qualcosa del genere: $this->service->setLoggedInUser("a-new-value"); Scriviamo in un test quello che vorrei poter fare con l’oggetto $this->service: function test_something() { $this->service->setLoggedInUser("a-new-value"); } L’IDE prontamente ci segnala il metodo come non esistente. Sfruttando la segnalazione dell’IDE faccio creare il metodo:
  • 13. Aggiungo una assert che richieda anche un effettiva implementazione del metodo: function test_something() { $this->service->setLoggedInUser("a-new-value"); self::assertEquals("a-new-value", $this->service->getLoggedInUser()); } PHPUnit 8.0.4 by Sebastian Bergmann and contributors. Runtime: PHP 7.2.15 with Xdebug 2.6.1 Configuration: /Users/andrea/trip-service-kata/php/phpunit.xml.dist Failed asserting that null matches expected 'a-new-value'. Expected :a-new-value Actual :null <Click to see difference> /Users/andrea/trip-service- kata/php/tests/TripServiceKata/Trip/TripServiceTest.php:40 Time: 95 ms, Memory: 6.00 MB FAILURES! Tests: 2, Assertions: 2, Failures: 1. F Scrivo velocemente l’implementazione: class TestableTripService extends TripService { private $loggedInUser; public function setLoggedInUser($loggedInUser) { $this->loggedInUser = $loggedInUser; } public function getLoggedInUser() { return $this->loggedInUser; } } PHPUnit 8.0.4 by Sebastian Bergmann and contributors.
  • 14. Runtime: PHP 7.2.15 with Xdebug 2.6.1 Configuration: /Users/andrea/trip-service-kata/php/phpunit.xml.dist Time: 85 ms, Memory: 4.00 MB OK (2 tests, 2 assertions) 10 Un nuovo test Scriviamo il nuovo test che dovrebbe attivare l’altro ramo dell’if: function test_something() { $this->service->setLoggedInUser("a-new-value"); $this->service->getTripsByUser(new User("")); } E passa: PHPUnit 8.0.4 by Sebastian Bergmann and contributors. Runtime: PHP 7.2.15 with Xdebug 2.6.1 Configuration: /Users/andrea/trip-service-kata/php/phpunit.xml.dist Time: 80 ms, Memory: 4.00 MB OK (2 tests, 1 assertion) Process finished with exit code 0 Se il test passa senza lanciare eccezioni vuol dire che questa volta l’esecuzione ha toccato un return della funzione. Prima di procedere oltre fissiamo un po’ di più questo test, aggiungiamo un asserzione sul valore di ritorno: function test_something() { $this->service->setLoggedInUser("a-new-value"); $trips_found = $this->service->getTripsByUser(new User("")); self::assertEquals("", $trips_found); } Notate che ho scritto asserzione senza cercare di capire prima quale fosse il valore giusto, anzi ho scelto apposta un valore che sono quasi sicuro sia sbagliato: la stringa vuota “” per valore che dovrebbe essere un array. Ho scelto un valore sbagliato perché in questo modo sfrutto il test in modo che mi dica lui quale è il valore giusto: con il suo messaggio di fallimento: PHPUnit 8.0.4 by Sebastian Bergmann and contributors. Runtime: PHP 7.2.15 with Xdebug 2.6.1 Configuration: /Users/andrea/trip-service-kata/php/phpunit.xml.dist
  • 15. Array () does not match expected type "string". /Users/andrea/trip-service- kata/php/tests/TripServiceKata/Trip/TripServiceTest.php:46 Time: 121 ms, Memory: 6.00 MB FAILURES! Tests: 2, Assertions: 2, Failures: 1. Mi dice che l’array non matcha con la stringa. Ora so cosa scrivere per fare passare il test: function test_something() { $this->service->setLoggedInUser("a-new-value"); $trips_found = $this->service->getTripsByUser(new User("")); self::assertEquals([], $trips_found); } Ora il test passa: PHPUnit 8.0.4 by Sebastian Bergmann and contributors. Runtime: PHP 7.2.15 with Xdebug 2.6.1 Configuration: /Users/andrea/trip-service-kata/php/phpunit.xml.dist Time: 89 ms, Memory: 4.00 MB OK (2 tests, 2 assertions) Process finished with exit code 0 Diamo un’occhiata alla copertura del codice:
  • 16. Adesso ci piacerebbe entrare nel prossimo ramo. Il contenuto del foreach non viene toccato, probabilmente perché $user->getFriends() restituisce un array vuoto. Diamo un occhiata alla classe User: class User { private $trips; private $friends; private $name; public function __construct($name) { $this->name = $name; $this->trips = array(); $this->friends = array(); } public function getTrips() { return $this->trips; } public function getFriends() { return $this->friends; } public function addFriend(User $user) { $this->friends[] = $user; } public function addTrip(Trip $trip) { $this->trips[] = $trip; } } È una classe molto semplice, il metodo getFriends() restituisce un array di amici dell’utente, e il
  • 17. metodo addFriend() popola la lista. Modifichiamo il test in modo che l’utente spiato abbia degli amici; function test_something() { $this->service->setLoggedInUser("a-new-value"); $utente_spiato = new User(""); $utente_spiato->addFriend(new User("un amico")); $utente_spiato->addFriend(new User("un altro amico")); $trips_found = $this->service->getTripsByUser($utente_spiato); self::assertEquals([], $trips_found); } Lanciamo i test e sono verdi. Controlliamo la copertura: Siamo riusciti ad entrare nel foreach. Non siamo riusciti ad entrare dentro l’if. Per entrare dentro l’if uno degli amici deve essere l’utente loggato. Creiamo un secondo test copiando e incollando quello esistente e modifichiamolo: function test_something_being_a_friend() { $loggedInUser = new User("a-new-value"); $this->service->setLoggedInUser($loggedInUser); $utente_spiato = new User(""); $utente_spiato->addFriend($loggedInUser); $trips_found = $this->service->getTripsByUser($utente_spiato); self::assertEquals([], $trips_found);
  • 18. } Vediamo il risultato dei test: tutti passano meno uno: PHPUnit 8.0.4 by Sebastian Bergmann and contributors. Runtime: PHP 7.2.15 with Xdebug 2.6.1 Configuration: /Users/andrea/trip-service-kata/php/phpunit.xml.dist TripServiceKataExceptionDependentClassCalledDuringUnitTestException : TripDAO should not be invoked on an unit test. /Users/andrea/trip-service-kata/php/src/TripServiceKata/Trip/TripDAO.php:12 /Users/andrea/trip-service-kata/php/src/TripServiceKata/Trip/TripService.php:23 /Users/andrea/trip-service- kata/php/tests/TripServiceKata/Trip/TripServiceTest.php:58 Time: 151 ms, Memory: 6.00 MB ERRORS! Tests: 3, Assertions: 2, Errors: 1. Process finished with exit code 2 Cerchiamo di capire cosa è successo guardando allo stack trace. La primo elemento dello stack vediamo che l’esecuzione che ha portato all’eccezione è partita dalla chiamata al nostro servizio nel nuovo test. /Users/andrea/trip-service-kata/php/tests/TripServiceKata/Trip/TripServiceTest.php:58 /Users/andrea/trip-service-kata/php/src/TripServiceKata/Trip/TripService.php:23 Dal secondo elemento dello stack trace vediamo che il nostro test ha attivato finalmente un pezzo di codice che non era stato ancora coperto. Dal terzo elemento vediamo che siamo arrivati a toccare il DAO, l’oggetto che in teoria dovrebbe accedere al database. Come prima, trattandosi di un esercizio, non è un vero DAO, è una versione abbozzata di un
  • 19. ipotetico DAO che invece di accedere al database lancia una eccezione. Come prima dobbiamo rompere questa dipendenza, come prima, prima di modificare il codice cerchiamo di coprirlo con un test che passa. Come prima usiamo expectException per caratterizzare il comportamento. function test_something_being_a_friend() { $loggedInUser = new User("a-new-value"); $this->service->setLoggedInUser($loggedInUser); $utente_spiato = new User(""); $utente_spiato→addFriend($loggedInUser); $this→expectException( DependentClassCalledDuringUnitTestException::class); $trips_found = $this->service->getTripsByUser($utente_spiato); self::assertEquals([], $trips_found); } Se lanciamo i test: tutto passa. Come prima dobbiamo cerchiamo di rompere la dipendeza con il pattern “Extract and Override Call”. Nel codice di seguito ho evidenziato il pezzo di codice da estrarre: public function getTripsByUser(User $user) { $tripList = array(); $loggedUser = $this->getLoggedInUser(); $isFriend = false; if ($loggedUser != null) { foreach ($user->getFriends() as $friend) { if ($friend == $loggedUser) { $isFriend = true; break; } } if ($isFriend) { $tripList = TripDAO::findTripsByUser($user); } return $tripList; } else { throw new UserNotLoggedInException(); } } Lo estraggo: public function getTripsByUser(User $user) { $tripList = array(); $loggedUser = $this->getLoggedInUser(); $isFriend = false; if ($loggedUser != null) { foreach ($user->getFriends() as $friend) { if ($friend == $loggedUser) { $isFriend = true;
  • 20. break; } } if ($isFriend) { $tripList = $this->loadTripsOfUser($user); } return $tripList; } else { throw new UserNotLoggedInException(); } } protected function loadTripsOfUser(User $user) { return TripDAO::findTripsByUser($user); } Ora possiamo agire sul TestableTripService per cambiare il comportamento: class TestableTripService extends TripService { ... protected function loadTripsOfUser(User $user) { return "trips of user"; } } Vediamo cosa succede a lanciare i test: PHPUnit 8.0.4 by Sebastian Bergmann and contributors. Runtime: PHP 7.2.15 with Xdebug 2.6.1 Configuration: /Users/andrea/trip-service-kata/php/phpunit.xml.dist 'trips of user' does not match expected type "array". /Users/andrea/trip-service-kata/php/tests/TripServiceKata/Trip/TripServiceTest.php:68 Time: 172 ms, Memory: 6.00 MB FAILURES! Tests: 3, Assertions: 3, Failures: 1. Come al solito la assertEquals ci dice quale dovrebbe essere il giusto valore. Modifichiamo il test inserendo il giusto valore ('trips of user') e lanciando i test. function test_something_being_a_friend() { $loggedInUser = new User("a-new-value"); $this->service->setLoggedInUser($loggedInUser); $utente_spiato = new User(""); $utente_spiato->addFriend($loggedInUser);
  • 21. $this- >expectException(DependentClassCalledDuringUnitTestException::class); $trips_found = $this->service->getTripsByUser($utente_spiato); self::assertEquals('trips of user', $trips_found); } Dopo aver lanciato i test otteniamo: PHPUnit 8.0.4 by Sebastian Bergmann and contributors. Runtime: PHP 7.2.15 with Xdebug 2.6.1 Configuration: /Users/andrea/trip-service-kata/php/phpunit.xml.dist Failed asserting that exception of type "TripServiceKataExceptionDependentClassCalledDuringUnitTestException" is thrown. Time: 114 ms, Memory: 6.00 MB FAILURES! Tests: 3, Assertions: 4, Failures: 1. I test ci stanno dicendo che ci siamo dimenticati un expectException che non serve più. function test_something_being_a_friend() { $loggedInUser = new User("a-new-value"); $this->service->setLoggedInUser($loggedInUser); $utente_spiato = new User(""); $utente_spiato->addFriend($loggedInUser); $this- >expectException(DependentClassCalledDuringUnitTestException::class); $trips_found = $this->service->getTripsByUser($utente_spiato); self::assertEquals('trips of user', $trips_found); } Rilanciamo i test e tutto passa. Diamo un occhiata ai nostri test: class TripServiceTest extends TestCase { /** @var TestableTripService */ private $service; protected function setUp(): void { $this->service = new TestableTripService(); } function test_only_logged_user_can_use_the_service() { $this->expectException(UserNotLoggedInException::class); $this->service->getTripsByUser(new User("")); } function test_something() { $this->service->setLoggedInUser("a-new-value");
  • 22. $utente_spiato = new User(""); $utente_spiato->addFriend(new User("un amico")); $utente_spiato->addFriend(new User("un altro amico")); $trips_found = $this->service->getTripsByUser($utente_spiato); self::assertEquals([], $trips_found); } function test_something_being_a_friend() { $loggedInUser = new User("a-new-value"); $this->service->setLoggedInUser($loggedInUser); $utente_spiato = new User(""); $utente_spiato->addFriend($loggedInUser); $trips_found = $this->service->getTripsByUser($utente_spiato); self::assertEquals('trips of user', $trips_found); } } Stiamo coprendo questi casi: • utente non loggato • utente loggato che cerca di vedere i contenuti di un utente non amico (di uno sconosciuto) • utente loggato che cerca di vedere i contenuti di un utente amico Diamo un occhiata alla copertura del codice.
  • 23. I test coprono completamente il metodo getTripsByUser(). I test lasciano fuori la parte che si interfaccia con i servizi esterni. Le due funzioni loadTripsOfUser() e getLoggedUser() sono dei boilerplate che abbiamo usato per disaccoppiarci dalle due dipendenze del servizio TripService. class TripServiceTest extends TestCase { /** @var TestableTripService */ private $service; protected function setUp(): void { $this->service = new TestableTripService(); } function test_only_logged_user_can_use_the_service() { $this->expectException(UserNotLoggedInException::class); $this->service->getTripsByUser(new User("")); } function test_something() { $this->service->setLoggedInUser("a-new-value");
  • 24. $utente_spiato = new User(""); $utente_spiato->addFriend(new User("un amico")); $utente_spiato->addFriend(new User("un altro amico")); $trips_found = $this->service->getTripsByUser($utente_spiato); self::assertEquals([], $trips_found); } function test_something_being_a_friend() { $loggedInUser = new User("a-new-value"); $this->service->setLoggedInUser($loggedInUser); $utente_spiato = new User(""); $utente_spiato->addFriend($loggedInUser); $trips_found = $this->service->getTripsByUser($utente_spiato); self::assertEquals('trips of user', $trips_found); } } 11 Adesso refactor! Adesso che il codice è coperto da test possiamo fare anche fare tutto il refactor che vogliamo. Da dove partiamo? Rifattorizzare anche i test I programmatori che usano il test automatico alla fine si trovano con la codebase idealmente (e spesso praticamente) divisa in due parti: • il codice di produzione e • il codice di test. Quale delle due parti è più importante? Quale parte del codice dobbiamo curare di più? Ho incontrato spesso team che capivano (almeno in teoria) l’importanza di avere del codice di produzione pulito ma che non considerava il codice di test altrettanto importante. Questi team di solito se fanno refactor lo fanno solo sul codice di produzione e lasciano indietro le possibili migliorie ai test. La mia esperienza è che i test non vanno considerati come codice di serie B, piuttosto conviene rifattorizzare tutto il codice. Consideriamo il codice di test creato durante questo esercizio. Adesso abbiamo definito tre test: • test_only_logged_user_can_use_the_service() • test_something() • test_something_being_a_friend() I nomi dei test sono un po’ criptici, un semplice primo refactor che faccio è quello di rinominare i casi di test: • test_only_logged_user_can_use_the_service() • test_logged_user_can_not_see_trips_of_a_stranger() • test_logged_user_can_see_trips_of_a_friend() Ora ci possiamo concentrare su ognuno dei singoli metodi di test partendo dal primo: function test_logged_user_can_not_see_trips_of_a_stranger() { $this->service->setLoggedInUser("a-new-value");
  • 25. $utente_spiato = new User(""); $utente_spiato->addFriend(new User("un amico")); $utente_spiato->addFriend(new User("un altro amico")); $trips_found = $this->service->getTripsByUser($utente_spiato); self::assertEquals([], $trips_found); } La prima modifica è organizzare il codice di test all’interno del metodo usando il pattern AAA (Arrange Act Assert). L’idea alla base del pattern AAA è che la maggior parte dei test può essere pensato in tre fasi: • Arrange: preparo l’oggetto sotto test e i vari collaboratori per l’esecuzione • Act: invoco il metodo sotto test. • Assert: verifico che il risultato dell’azione si quello che ci si aspettava Nel nostro caso il codice può essere raggruppato in questo modo: function test_logged_user_can_not_see_trips_of_a_stranger() { // Arrange $this->service->setLoggedInUser("a-new-value"); $utente_spiato = new User(""); $utente_spiato->addFriend(new User("un amico")); $utente_spiato->addFriend(new User("un altro amico")); // Act $trips_found = $this→service→getTripsByUser($utente_spiato); // Assert self::assertEquals([], $trips_found); } Nella parte di “Arrange” ci sono queste ben tre linee spese per affermare un solo concetto: l’utente di cui vengono spiati i viaggi in questo test è uno sconosciuto. $utente_spiato = new User(""); $utente_spiato->addFriend(new User("un amico")); $utente_spiato->addFriend(new User("un altro amico")); Un modo più semplice per esprime questo concetto è usare una factory: function test_logged_user_can_not_see_trips_of_a_stranger() { // Arrange $this->service->setLoggedInUser("a-new-value"); $utente_spiato = $this->unoSconosciuto(); // Act $trips_found = $this->service->getTripsByUser($utente_spiato); // Assert self::assertEquals([], $trips_found); } private function unoSconosciuto() { $sconosciuto = new User(""); $sconosciuto->addFriend(new User("un amico")); $sconosciuto->addFriend(new User("un altro amico"));
  • 26. return $sconosciuto; } Volendo si può anche mettere inline: function test_logged_user_can_not_see_trips_of_a_stranger() { // Arrange $this->service->setLoggedInUser("a-new-value"); // Act $trips_found = $this->service->getTripsByUser($this->unoSconosciuto()); // Assert self::assertEquals([], $trips_found); } A questo punto è giunto il momento di svelarvi un segreto riguardo al pattern Arrange Act Assert: dato che le tre fasi si ripetono praticamente sempre uguali nei test in genere si evita di mettere i commenti e le tre fasi si evidenziano semplicemente lasciando una riga vuota tra una fase e l’altra: function test_logged_user_can_not_see_trips_of_a_stranger() { $this->service->setLoggedInUser("a-new-value"); $trips_found = $this->service->getTripsByUser($this->unoSconosciuto()); self::assertEquals([], $trips_found); } Un ultima cosa che mi fa storcere il naso è la stringa che rappresenta l’utente loggato, possiamo usare un oggetto User e usare un campo dell’oggetto: /** @var User */ private $a_registered_user; protected function setUp(): void { ... $this->a_registered_user = new User("a registered user"); } ... function test_logged_user_can_not_see_trips_of_a_stranger() { $this->service->setLoggedInUser($this->a_registered_user); $trips_found = $this->service->getTripsByUser($this->unoSconosciuto()); self::assertEquals([], $trips_found); } 12 Refactor del secondo test Adesso il test appare così:
  • 27. function test_logged_user_can_see_trips_of_a_friend() { $loggedInUser = new User("a-new-value"); $this->service->setLoggedInUser($loggedInUser); $utente_spiato = new User(""); $utente_spiato->addFriend($loggedInUser); $trips_found = $this->service->getTripsByUser($utente_spiato); self::assertEquals('trips of user', $trips_found); } Il pezzo di codice in mezzo: $utente_spiato = new User(""); $utente_spiato->addFriend($loggedInUser); serve per creare un utente amico dell’utente loggato. Rendiamo evidente questa intenzione. Per farlo potremmo usare un commento: function test_logged_user_can_see_trips_of_a_friend() { $loggedInUser = new User("a-new-value"); $this->service->setLoggedInUser($loggedInUser); // crea un amico dell'utente loggato $utente_spiato = new User(""); $utente_spiato->addFriend($loggedInUser); $trips_found = $this->service->getTripsByUser($utente_spiato); self::assertEquals('trips of user', $trips_found); } In realtà un commento si può quasi sempre trasformare nel nome di un metodo: function test_logged_user_can_see_trips_of_a_friend() { $loggedInUser = new User("a-new-value"); $this->service->setLoggedInUser($loggedInUser); $utente_spiato = $this->unAmicoDi($loggedInUser); $trips_found = $this->service->getTripsByUser($utente_spiato); self::assertEquals('trips of user', $trips_found); } private function unAmicoDi(User $loggedInUser) { $utente_amico = new User(""); $utente_amico->addFriend($loggedInUser); return $utente_amico; } Poi la prima parte in realtà può essere rivista usando la proprietà $a_registered_user creata prima:
  • 28. function test_logged_user_can_see_trips_of_a_friend() { $this->service->setLoggedInUser($this->a_registered_user); $utente_spiato = $this->unAmicoDi($this->a_registered_user); $trips_found = $this->service->getTripsByUser($utente_spiato); self::assertEquals('trips of user', $trips_found); } Vediamo l’ultimo test: function test_only_logged_user_can_use_the_service() { $this->expectException(UserNotLoggedInException::class); $this->service->getTripsByUser(new User("")); } In questo test si dice che ci si aspetta una eccezione UserNotLoggedInException però nel testo del test per me non è abbastanza evidente quando questo succede. Quello che sappiamo è che l’eccezione avviene: 1) quando l’utente non è loggato e 2) qualunque sia l’utente spiato. Modifichiamo il test in modo che queste due informazioni siano più chiare: function test_only_logged_user_can_use_the_service() { $this->nessunUtenteLoggato(); $utente_spiato = $this->unUtenteQualunque(); $this->expectException(UserNotLoggedInException::class); $this->service->getTripsByUser($utente_spiato); } private function nessunUtenteLoggato(): void { return $this->service->setLoggedInUser(null); } private function unUtenteQualunque() { return new User("uno qualunque"); } Arrange-Act-Assert oppure Arrange-Expect-Act? Notate che questo test non è organizzato secondo Arrange-Act-Assert proprio perché, in questo caso, non c’è una asserzione ma un aspettativa(expect). La suddivisioni in fasi Arrange-Expect-Act è tipica dei test i framework di mock basati sulla definizione di aspettative (expectations). 13 Refactor del codice di produzione Ora passiamo al refactor del codice di produzione. class TripService {
  • 29. public function getTripsByUser(User $user) { $tripList = array(); $loggedUser = $this->getLoggedInUser(); $isFriend = false; if ($loggedUser != null) { foreach ($user->getFriends() as $friend) { if ($friend == $loggedUser) { $isFriend = true; break; } } if ($isFriend) { $tripList = $this->loadTripsOfUser($user); } return $tripList; } else { throw new UserNotLoggedInException(); } } protected function loadTripsOfUser(User $user) { return TripDAO::findTripsByUser($user); } protected function getLoggedInUser() { return UserSession::getInstance()->getLoggedUser(); } } Ci sono due grandi aspetti di questo codice che chiedono di essere sistemati: • il corpo del metodo principale è molto complesso • le dipendenze non solo non sono iniettate ma dipendono direttamente da dettagli implementativi (come lo specifico framework MVC, e la specifica classe che si interfaccia al database). Mi concentro per primo sul design del metodo: public function getTripsByUser(User $user) { $tripList = array(); $loggedUser = $this->getLoggedInUser(); $isFriend = false; if ($loggedUser != null) { foreach ($user->getFriends() as $friend) { if ($friend == $loggedUser) { $isFriend = true; break; } } if ($isFriend) { $tripList = $this->loadTripsOfUser($user); } return $tripList; } else { throw new UserNotLoggedInException(); }
  • 30. } Comincio con la prima linea: $tripList = array(); Qui inzializzaziamo una variable che viene usata solo molto dopo nel codice. La sposto: public function getTripsByUser(User $user) { $loggedUser = $this->getLoggedInUser(); $isFriend = false; if ($loggedUser != null) { foreach ($user->getFriends() as $friend) { if ($friend == $loggedUser) { $isFriend = true; break; } } $tripList = array(); if ($isFriend) { $tripList = $this->loadTripsOfUser($user); } return $tripList; } else { throw new UserNotLoggedInException(); } } Mi concentro su questo blocco: $tripList = array(); if ($isFriend) { $tripList = $this->loadTripsOfUser($user); } return $tripList; Potrebbe essere espresso senza usare la variabile temporanea: if ($isFriend) { return $this->loadTripsOfUser($user); } return array(); E poi la riga: return array(); Si può esprimere usando il literal per gli array vuoti: Torniamo a dare uno sguardo di insieme al metodo: public function getTripsByUser(User $user) { $loggedUser = $this->getLoggedInUser(); $isFriend = false; if ($loggedUser != null) { foreach ($user->getFriends() as $friend) { if ($friend == $loggedUser) {
  • 31. $isFriend = true; break; } } if ($isFriend) { return $this->loadTripsOfUser($user); } return []; } else { throw new UserNotLoggedInException(); } } Anche variabile $isFriend è inizializzata molto prima di essere usata, possiamo spostarla giù. public function getTripsByUser(User $user) { $loggedUser = $this->getLoggedInUser(); if ($loggedUser != null) { $isFriend = false; foreach ($user->getFriends() as $friend) { if ($friend == $loggedUser) { $isFriend = true; break; } } if ($isFriend) { return $this->loadTripsOfUser($user); } return []; } else { throw new UserNotLoggedInException(); } } Mi concentro su blocco: $isFriend = false; foreach ($user->getFriends() as $friend) { if ($friend == $loggedUser) { $isFriend = true; break; } } E capisco che in realtà il suo unico scopo è calcolare unico boolean che mi dice se $loggedUser e amico di $user. Aggiungo prima un commento: // determine if $user is friend of $loggedUser $isFriend = false; foreach ($user->getFriends() as $friend) { if ($friend == $loggedUser) { $isFriend = true; break; } }
  • 32. Poi lo trasformo in una chiamata a metodo. È una cosa incrementale prima aggiungo una chiamata ad un metodo fantasma: $isFriend = $user->isFriendOf($loggedUser); // determine if $user is friend of $loggedUser $isFriend = false; foreach ($user->getFriends() as $friend) { if ($friend == $loggedUser) { $isFriend = true; break; } } Se lancio i test giustamente falliscono perché User# isFriendOf non esiste. Error : Call to undefined method TripServiceKataUserUser::isFriendOf() /Users/andrea/trip-service-kata/php/src/TripServiceKata/Trip/TripService.php:15 /Users/andrea/trip-service-kata/php/tests/TripServiceKata/Trip/TripServiceTest.php:66 Faccio passare i test creando il metodo vuoto: ... class User { ... function isFriendOf($loggedUser) { } } Poi gli copio dentro il contenuto del blocco: function isFriendOf($loggedUser) { // determine if $user is friend of $loggedUser $isFriend = false; foreach ($user->getFriends() as $friend) { if ($friend == $loggedUser) { $isFriend = true; break; } } } Prima di lanciare i test sistemo un po’: function isFriendOf($loggedUser) { // determine if $user is friend of $loggedUser $isFriend = false; foreach ($this->getFriends() as $friend) { if ($friend == $loggedUser) { $isFriend = true; break; }
  • 33. } return $isFriend; } 14 Invertire gli if Siamo arrivati a questa situazione: public function getTripsByUser(User $user) { $loggedUser = $this->getLoggedInUser(); if ($loggedUser != null) { $isFriend = $user->isFriendOf($loggedUser); if ($isFriend) { return $this->loadTripsOfUser($user); } return []; } else { throw new UserNotLoggedInException(); } } L’if più esterno può essere invertito e trasformato in una guardia: public function getTripsByUser(User $user) { $loggedUser = $this->getLoggedInUser(); if ($loggedUser == null) throw new UserNotLoggedInException(); $isFriend = $user->isFriendOf($loggedUser); if ($isFriend) { return $this->loadTripsOfUser($user); } return []; } Della variabile $isFriend si può fare l’inline. public function getTripsByUser(User $user) { $loggedUser = $this->getLoggedInUser(); if ($loggedUser == null) throw new UserNotLoggedInException(); if ($user->isFriendOf($loggedUser)) { return $this->loadTripsOfUser($user); } return []; } Anche il secondo if si può invertire: public function getTripsByUser(User $user) { $loggedUser = $this->getLoggedInUser(); if ($loggedUser == null) throw new UserNotLoggedInException();
  • 34. if (!$user->isFriendOf($loggedUser)) return []; return $this->loadTripsOfUser($user); } Alla fine abbiamo due guardie a cui segue l’esecuzione dell’happy path. 15 Iniettare le dipendenze Il codice è questo: public function getTripsByUser(User $user) { $loggedUser = $this->getLoggedInUser(); if ($loggedUser == null) throw new UserNotLoggedInException(); if (!$user->isFriendOf($loggedUser)) return []; return $this->loadTripsOfUser($user); } ... protected function getLoggedInUser() { return UserSession::getInstance()->getLoggedUser(); } Una che stona un po’ è il fatto che il servizio per funzionare avrebbe bisogno del valore $loggedUser e per recuperarlo fa due lookup (uno per accedere singleton UserSession::getInstance() e un altro per accedere al getLoggedUser()) . Io in questi casi preferisco usare l’iniezione delle dipendenze rispetto al lookup. Sarebbe tutto più semplice se la signature del metodo getTripsByUser() prevedesse già il parametro $loggedUser. Qualcosa del tipo: public function getTripsByUser(User $user, User? $loggedUser) ... Questo però sarebbe un cambiamento all’interfaccia del metodo, si può fare ma bisogna tenere conto che i test che abbiamo adesso coprono solo il singolo oggetto TripService ma non abbiamo nessun test che verifica come le altre classi si integrano con TripService. Questo refactor è grandino e si può splittare in step: • Aggiunta del parametro $loggedUser a getTripsByUser() (senza ancora usarlo) • Switch dall’uso attuale (lookup) all’uso nuovo (parametro) • Pulizia delle parti non più necessarie. 16 Step 1/3: Aggiunta del parametro senza ancora usarlo Prima di tutto aggiungiamo il parametro: public function getTripsByUser(User $user, $loggedUser) Se lanciamo i test adesso non passeranno ma il compilatore ci farà il piacere di dirci tutti i punti
  • 35. dove bisogna aggiungere il parametro: Testing started at 15:42 ... /usr/local/Cellar/php@7.2/7.2.15/bin/php /Users/andrea/trip-service- kata/php/vendor/phpunit/phpunit/phpunit --configuration /Users/andrea/trip- service-kata/php/phpunit.xml.dist TripServiceKataTripTripServiceTest /Users/andrea/trip-service- kata/php/tests/TripServiceKata/Trip/TripServiceTest.php --teamcity PHPUnit 8.0.4 by Sebastian Bergmann and contributors. Runtime: PHP 7.2.15 with Xdebug 2.6.1 Configuration: /Users/andrea/trip-service-kata/php/phpunit.xml.dist ArgumentCountError : Too few arguments to function TripServiceKataTripTripService::getTripsByUser(), 1 passed in /Users/andrea/trip-service- kata/php/tests/TripServiceKata/Trip/TripServiceTest.php on line 66 and exactly 2 expected /Users/andrea/trip-service-kata/php/src/TripServiceKata/Trip/TripService.php:11 /Users/andrea/trip-service- kata/php/tests/TripServiceKata/Trip/TripServiceTest.php:66 ArgumentCountError : Too few arguments to function TripServiceKataTripTripService::getTripsByUser(), 1 passed in /Users/andrea/trip-service- kata/php/tests/TripServiceKata/Trip/TripServiceTest.php on line 75 and exactly 2 expected /Users/andrea/trip-service-kata/php/src/TripServiceKata/Trip/TripService.php:11 /Users/andrea/trip-service- kata/php/tests/TripServiceKata/Trip/TripServiceTest.php:75 Failed asserting that exception of type "ArgumentCountError" matches expected exception "TripServiceKataExceptionUserNotLoggedInException". Message was: "Too few arguments to function TripServiceKataTripTripService::getTripsByUser(), 1 passed in /Users/andrea/trip-service- kata/php/tests/TripServiceKata/Trip/TripServiceTest.php on line 49 and exactly 2 expected" at /Users/andrea/trip-service-kata/php/src/TripServiceKata/Trip/TripService.php:11 /Users/andrea/trip-service- kata/php/tests/TripServiceKata/Trip/TripServiceTest.php:49 . Time: 99 ms, Memory: 6.00 MB ERRORS! Tests: 3, Assertions: 1, Errors: 2, Failures: 1. Process finished with exit code 2 Sistemo gli errori del compilatore uno alla volta (questa tecnica si dice “Lean on compiler”): Primo punto: function test_only_logged_user_can_use_the_service() { $this->nessunUtenteLoggato(); $utente_spiato = $this->unUtenteQualunque();
  • 36. $this->expectException(UserNotLoggedInException::class); $this->service->getTripsByUser($utente_spiato, null); } Secondo punto: function test_logged_user_can_not_see_trips_of_a_stranger() { $this->service->setLoggedInUser($this->a_registered_user); $utente_spiato = $this->unoSconosciuto(); $trips_found = $this->service->getTripsByUser($utente_spiato, $this- >a_registered_user); self::assertEquals([], $trips_found); } Terzo punto: function test_logged_user_can_see_trips_of_a_friend() { $this->service->setLoggedInUser($this->a_registered_user); $utente_spiato = $this->unAmicoDi($this->a_registered_user); $trips_found = $this->service->getTripsByUser($utente_spiato, $this- >a_registered_user); self::assertEquals('trips of user', $trips_found); } Ora se lancio i test passano tutti. 17 Step 2/3: Switch alla nuova versione Il codice del servizio adesso è: public function getTripsByUser(User $user, $loggedUser) { $loggedUser = $this->getLoggedInUser(); if ($loggedUser == null) throw new UserNotLoggedInException(); if (!$user->isFriendOf($loggedUser)) return []; return $this->loadTripsOfUser($user); } Al metodo arriva il parametro ma non viene ancora usato, anzi si usa il metodo vecchio. Per passare al nuovo modo mi basta cancellare la riga che chima getLoggedInUser(). public function getTripsByUser(User $user, $loggedUser) { $loggedUser = $this->getLoggedInUser(); if ($loggedUser == null) throw new UserNotLoggedInException(); if (!$user->isFriendOf($loggedUser)) return []; return $this->loadTripsOfUser($user); } Lancio i test e tutti passano. 18 Step 3/3: pulizia del modo vecchio A questo punto abbiamo tutti i test che passano ma ci è rimasto del codice che non serve più. Passo
  • 37. a fare pulizia e ad ogni passo lancio i test. Cancello il metodo getLoggedInUser() da TripService: class TripService { public function getTripsByUser(User $user, $loggedUser) { if ($loggedUser == null) throw new UserNotLoggedInException(); if (!$user->isFriendOf($loggedUser)) return []; return $this->loadTripsOfUser($user); } protected function loadTripsOfUser(User $user) { ... } protected function getLoggedInUser() { return UserSession::getInstance()->getLoggedUser(); } } Lo cancello da TestableTripService: class TestableTripService extends TripService { private $loggedInUser; public function setLoggedInUser($loggedInUser) { $this->loggedInUser = $loggedInUser; } public function getLoggedInUser() { return $this->loggedInUser; } protected function loadTripsOfUser(User $user) { return "trips of user"; } } Lancio i test e tutti passano. A questo punto non è neanche più necessario il metodo setLoggedInUser(). Prima però devo cancellare tutte le sue chiamate. function test_only_logged_user_can_use_the_service() { $this->nessunUtenteLoggato(); $utente_spiato = $this->unUtenteQualunque(); $this->expectException(UserNotLoggedInException::class); $this->service->getTripsByUser($utente_spiato, null); } private function nessunUtenteLoggato() { $this->service->setLoggedInUser(null); }
  • 38. private function unUtenteQualunque() { return new User("uno qualunque"); } function test_logged_user_can_not_see_trips_of_a_stranger() { $this->service->setLoggedInUser($this->a_registered_user); $utente_spiato = $this->unoSconosciuto(); $trips_found = $this->service->getTripsByUser($utente_spiato, $this- >a_registered_user); self::assertEquals([], $trips_found); } function test_logged_user_can_see_trips_of_a_friend() { $this->service->setLoggedInUser($this->a_registered_user); $utente_spiato = $this->unAmicoDi($this->a_registered_user); $trips_found = $this->service->getTripsByUser($utente_spiato, $this- >a_registered_user); self::assertEquals('trips of user', $trips_found); } Mi accorgo anche che adesso il metodo nessunUtenteLoggato() risulta vuoto, quindi posso buttare via lui e l’unica sua chiamata rimasta. function test_only_logged_user_can_use_the_service() { $this->nessunUtenteLoggato(); $utente_spiato = $this->unUtenteQualunque(); $this->expectException(UserNotLoggedInException::class); $this->service->getTripsByUser($utente_spiato, null); } private function nessunUtenteLoggato() { } Ora che ho rimosso tutte le chiamate posso tornare su TestableTripService e buttare via setLoggedInUser(). class TestableTripService extends TripService { private $loggedInUser; public function setLoggedInUser($loggedInUser) { $this->loggedInUser = $loggedInUser; } protected function loadTripsOfUser(User $user) { return "trips of user"; } } Lancio i test e tutti passano.
  • 39. 19 Iniettare la dipendenza per l’accesso al DB TBD