Documente Academic
Documente Profesional
Documente Cultură
Fernando Arconada
Este libro est a la venta en http://leanpub.com/testingsymfony2
Esta versin se public en 2015-02-23
This is a Leanpub book. Leanpub empowers authors and publishers with the Lean Publishing
process. Lean Publishing is the act of publishing an in-progress ebook using lightweight tools and
many iterations to get reader feedback, pivot until you have the right book and build traction once
you do.
2013 - 2015 Fernando Arconada
ndice general
Por qu testing? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6
7
11
100% de Cobertura? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
13
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
15
15
15
16
16
17
18
18
20
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
28
28
37
43
50
DataFixutures . . . . . . . . . . . .
Entornos de ejecucin en Symfony
DoctrineFixturesBundle . . . . . .
Faker . . . . . . . . . . . . . . . .
Alice . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
57
57
58
62
64
Matthias Noback . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
68
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
NDICE GENERAL
La experiencia de testing con PHP: entrevista a Matthias Noback por Fernando Arconada .
68
.
.
.
.
.
.
.
.
.
.
.
.
74
74
74
74
75
75
75
75
76
76
76
77
.
.
.
.
77
77
78
78
.
.
.
.
.
.
.
.
79
79
80
81
81
81
82
82
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Por qu testing?
La respuesta es sencilla: porque ya lo hacemos desde que empezamos a programar. Tecleas unas
lneas de cdigo, abres el navegador, rellenas un formulario con los datos que se te ocurren para
probar y lo envas a ver si has hecho todo bien.
El nico problema de todo esto es que los test los creas y se ejecutan de forma manual.
Si podemos hacer que los tests los ejecute un ordenador entonces podremos ejecutarlos de forma
continuada en un ciclo segn vayamos desarrollando la aplicacin. Esto nos dar un nivel de
confianza muy alto sobre el funcionamiento de lo que estamos desarrollando.
La regla bsica es escribir tests que demuestren que el cdigo funciona como se espera. Pero este no
hay ms motivos para escribir tests de software:
Verifican de forma automtica que las especificaciones que hemos definido se cumplen.
Mejoran la documentacin, la forma de escribir las especificaciones en herramientas como
Behat hace que la descripcin de la funcionalidad y los casos limite sean fcilmente entendibles
incluso para personas no tcnicas.
Hacen que trabajar en equipo sea mas sencillo y seguro, puesto que es fcil emplear y modificar
piezas de software de terceros sabiendo que todo sigue funcionando
Permiten evolucionar el software de forma segura. Por muy buena que sea la documentacin
de una aplicacin cuando pasa mucho tiempo nos vamos olvidando de ella y suele ser entonces
cuando nos toca hacer cambios. Cambios que hacemos cruzando los dedos y que an despus
de haber probado la pgina a la que creemos que afecta no tenemos la completa certeza de
que no se haya roto nada en otro sitio. Con los test automatizados de software evitamos en
gran parte este problema porque el ordenador va a probar por nosotros todas las partes de la
aplicacin que consideramos que debera ser probadas el da que los creamos.
Ahorran tiempo. Este punto resulta controvertido sobre todo en aplicaciones pequeas, en
aplicaciones que consideramos que no van a evolucionar. El problema es determinar una
aplicacin pequea y tener la certeza de que algo para a permanecer as para siempre. Para un
script de administracin de un servidor no escribo tests, quizs debera. Cuando parece que no
hay duda es cuando estamos delante de una aplicacin grande, de las llamadas vivas o muy
vivas, ciertamente en estos casos no solo ahorran tiempo si no que de no hacerlo acabaremos
con una bola de cdigo de esas que al cabo del tiempo dan miedo y que son el padre de la frase
si funciona no lo toques
Escribir tests de software tambin tiene inconvenientes, sobre todo si acabamos de empezar:
Se pierde tiempo aprendiendo cmo escribir un test.
Por qu testing?
Ciertamente escribir y mantener los tests cuesta tiempo, pero si la aplicacin merece la pena (no
todas las aplicaciones merecen este esfuerzo extra, por ejemplo porque va durar muy poco), al final
la balanza entre el tiempo perdido y el ahorrado siempre acaba de nuestro lado.
Con respecto a lo de la falsa sensacin de seguridad, se supone que con los tests garantizamos que
todo funcione no?. S, pero slo con los buenos tests, aquellos que verifican al completo los posibles
casos de una pieza de cdigo y tienen las aserciones correctas.
Un test que hace alguna de estas cosas no es malo de por si, pero ser un test de integracin o
funcional.
En Symfony ya est habilitada por defecto la integracin con PHPUnit, pero tambin podemos
escribir tests unitarios con PHPSpec fcilmente.
Un test de integracin es en el que se prueba la interaccin entre dos o ms unidades. Los tests de
integracin prueban si dos componentes pueden trabajar juntos. Por ejemplo, el uso de una librera
de terceros y un mtodo nuestro.
Un test funcional es un tipo especial de test de integracin que ejecuta la aplicacin desde un punto
de vista del usuario. Por ejemplo, un usuario rellena una formulario, pulsa en envar y se genera un
email.
No creo que sea necesaria distinguir entre test funcionales y de integracin. Los test funcionales de
Symfony no los distinguen.
Un test funcional no es muy diferente de uno unitario, se distinguen en que siguen los siguientes
pasos:
haces un request
compruebas la respuesta
haces click en un link o en envas un formulario
compruebas la respuesta
repetir desde el inicio
Un ejemplo:
http://www.artima.com/weblogs/viewpost.jsp?thread=126923
http://symfony.com/doc/current/book/testing.html
http://symfony.com/doc/current/book/testing.html#functional-tests
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
// src/Acme/DemoBundle/Tests/Controller/DemoControllerTest.php
namespace Acme\DemoBundle\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class DemoControllerTest extends WebTestCase
{
public function testIndex()
{
$client = static::createClient();
$crawler = $client->request('GET', '/demo/hello/Fabien');
$this->assertGreaterThan(
0,
$crawler->filter('html:contains("Hello Fabien")')->count()
);
}
}
Los test de aceptacin no son muy diferentes de los test funcionales, la principal diferencia es
que los tests de aceptacin estn dirigidos a usuarios no tcnicos y se escriben un lenguaje fcil de
comprender.
Por ejemplo en Codeception:
1
2
3
4
5
6
7
8
9
<?php
$I = new WebGuy($scenario);
$I->wantTo('sign in');
$I->amOnPage('/login');
$I->fillField('signin[username]', 'davert');
$I->fillField('signin[password]','qwerty');
$I->click('LOGIN');
$I->see('Welcome, Davert!');
?>
Podemos escribir los test funcionales con BeHat o Codeception. O mejor, en mi opinin, podemos
prescindir de los test funcionales si tenemos buenos tests de aceptacin. Para que estos tests de
aceptacin sean buenos debemos incluir tests que se centren en los aspectos que nos interesen a los
desarrolladores y que a un usuario es posible que no le importen.
http://behat.org/
http://codeception.com/
Ciclo TDD
Ciclo de TDD/BDD
Una parte del ciclo es funcional, y ah describimos desde un punto de vista de ms alto nivel
la caracterstica que queremos implementar. Hay otra parte en ciclo en la que bajamos a nivel de
cdigo, preferiblemente a nivel unitario de un mtodo y testeamos esas pocas lneas de software.
Seguir el ciclo de desarrollo de TDD/BDD no impone una herramienta, y de hecho, no tenemos
por qu elegir ninguna. Podramos desarrollar ejecutando todos los tests de forma manual aunque
nuestro inters est en ejecutar los test de forma automtica.
StoryBDD
Para especificar el comportamiento de alto nivel del software nos expresamos en un lenguaje de alto
nivel, lo ms cercano posible a la forma que nos expresamos de persona a persona. Intentaremos
expresarnos como si hablsemos con el cliente. Contaremos una historia en muy pocas palabras y
sin ambigedades.
A esta parte del ciclo de desarrollo TDD en el que describimos el comportamiento de alto nivel le
llamaremos StoryBDD.
El lenguaje empleado puede ser asi:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
En una primera parte se describe el comportamiento que queremos lograr (Feature) y posteriormente
se definen una serie de criterios de aceptacin (Scenario) en el que se expresan las precondiciones y
postcondiciones que se deben cumplir para dar por buena la funcionalidad.
Hay herramientas como Behat que permiten escribir test automticos en un lenguaje natural como
el del ejemplo anterior de forma que sea totalmente legible para el usuario y as poder convertir
estos test en test de aceptacin. Los tests de aceptacin sirven para verificar el cumplimiento de los
requisitos solicitados por el cliente.
Given/When/Then
El lenguaje con el que describimos los escenarios en Behat se llama Gherkin . Es el mismo
lenguaje que se emplea en Cucumber (una popular herramienta de BDD de Ruby). Como Gherkin
es parecido al lenguaje hablado, pero fijando ciertas estructuras ofrece una forma de que todas las
personas implicadas en el desarrollo (incluido el cliente), puedan describir los requerimientos con
una expresin similar. As es ms fcil que se comuniquen todos los participantes.
En Gherkin se expresan los escenarios en trminos de Given/When/Then que es la conexin con
las expresiones humanas de causa y efecto. En lenguaje de computadores sera el equivalente a
input/process/output.
Given
El objetivo de Given es poner el sistema en un estado conocido antes de que el usuario empiece a
interactuar con l. En este punto hay que tratar de evitar las interacciones de usuario.
Ejemplos:
http://behat.org/
https://github.com/cucumber/cucumber/wiki/Given-When-Then
Scenario: Hay una URL que devuelve una lista de tareas en JSON
When I go to "/tareas/lista"
Then the response status code should be 200
And the header "Content-Type" should be equal to "application/json"
And the response should be in JSON
And the JSON node "root" should have 20 elements
10
<?php
$I = new WebGuy($scenario);
$I->wantTo('sign in');
$I->amOnPage('/login');
$I->fillField('signin[username]', 'davert');
$I->fillField('signin[password]','qwerty');
$I->click('LOGIN');
$I->see('Welcome, Davert!');
Esta sintaxis, que es directamente cdigo, resulta prcticamente igual de fcil de leer para los
desarrolladores y no tienen que cambiar su forma de expresarse. Adems, se puede utilizar el code
completion de los IDE de desarrollo.
Con un sencillo comando los escenarios escritos con Codeception se pueden convertir a lenguaje
natural para ser entregado a personal no tcnico.
1
2
3
4
5
6
7
8
WANT TO SIGN IN
am on page '/login'
fill field ['signin[username]', 'davert']
fill field ['signin[password]', 'qwerty']
click 'LOGIN'
see 'Welcome, Davert!'
http://codeception.com/
11
SpecBDD
Si StoryBDD se centra en la especificacin a nivel de negocio/funcional, SpecBDD se centra
en la especificacin a nivel de cdigo. Lo que comnmente conocemos como test unitario. Una
herramienta de StoryBDD debe dejar clara la narrativa, lo que pretendemos conseguir. Una
herramienta SpecBDD est enfocada a cmo conseguir esa funcionalidad: la implementacin.
La verdadera diferencia entre cualquier herramienta para ejecutar tests unitarios xUnit, por ejemplo
PHPUnit, y una herramienta de tipo xSpec, como PHPSpec es el lenguaje empleado.
Con un herramienta xUnit escribes una asercin de un test:
1
PHPSpec especifica ms claramente lo que debe hacer nuestra clase a testear, de forma que se
mantiene la ubicuidad del lenguaje al expresar el comportamiento como lo haramos al expresarnos
de forma natural.
Otra diferencia importante es que las herramientas tipo xSpec se centran ms en el comportamiento
que las herramientas xUnit, que suelen estar ms centradas en la estructura del cdigo.
Como se puede ver en el ejemplo de Konstantin Kudryashov
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class UserRatingCalculator
{
private $dispatcher;
public function __construct(EventDispatcher $dispatcher)
{
$this->dispatcher = $dispatcher;
}
public function increaseUserRating(User $user, $add = 1)
{
$this->dispatcher->userRatingIncreasing($user->getRating());
$user->setRating($user->getRating() + $add);
$this->dispatcher->userRatingIncreased($user->getRating());
}
}
http://everzet.com/post/72910908762/conceptual-difference-between-mockery-and-prophecy
12
<?php
$user = Mockery::mock('User');
$user->shouldReceive('getRating')->andReturn(2, 2, 4);
$user->shouldReceive('setRating')->with(4)->once();
$disp = Mockery::mock('EventDispatcher');
$disp->shouldReceive('userRatingIncreasing')->with(2)->once();
$disp->shouldReceive('userRatingIncreased')->with(4)->once();
$calc = new UserRatingCalculator($disp->mock());
$calc->increaseUserRating($user->mock(), 2);
El test est vinculado a la estructura (al nmero de lnea) en que se llama getRating() puesto que el
mock devolver 2, 2 y 4 en ese orden.
Con PHPSpec y Phrophecy el test no depende del orden:
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$user = $prophet->prophesize('User');
$user->getRating()->willReturn(2);
$user->setRating(Argument::type('integer'))->will(function($rating) {
$this->getRating()->willReturn($rating);
})->shouldBeCalled();
$disp = $prophet->prophesize('EventDispatcher');
$disp->userRatingIncreasing(2)->shouldBeCalled();
$disp->userRatingIncreased(4)->shouldBeCalled();
$calc = new UserRatingCalculator($disp->reveal());
$calc->increaseUserRating($user->reveal(), 2);
100% de Cobertura?
La cobertura de cdigo es una medida empleada para describir el porcentaje de lneas que han sido
ejecutadas por los tests.
La librera de Sebastian Bergmann, autor de PHPUnit, php-code-coverage provee de la funcionalidad necesaria para calcular las mtricas de cobertura y mostrarlas en forma de una web en la que
podamos explorar nuestro cdigo.
Si seguimos estrictamente una prctica de desarrollo de TDD o BDD tendremos siempre una
cobertura del 100% del cdigo puesto que debemos escribir los test y luego el cdigo. Slo
escribiremos el cdigo justo para pasar los tests.
Que tengamos un 100% de cobertura no quiere decir que el cdigo est probado perfectamente. Eso
depende de la calidad de los test y de la correccin de las aserciones.
Un ejemplo de test con PHPSpec:
https://github.com/sebastianbergmann/php-code-coverage
100% de Cobertura?
1
2
3
4
5
6
7
14
<?php
// cobertura 100%
public function it_should_have_a_list_action(TaskRepository $taskRepo) {
$taskRepo->findAll()->shouldBeCalled();
$response = $this->listAction();
$response->shouldHaveType('Symfony\Component\HttpFoundation\Response');
}
Este test va a tener un 100% de cobertura puesto que el mtodo listAction() se ejecuta por todas
sus lneas.
1
2
3
4
5
<?php
// tambien cobertura 100%
public function it_should_have_a_list_action(TaskRepository $taskRepo) {
$response = $this->listAction();
}
Este otro test tambin obtiene una cobertura del 100% puesto que pasa por todas las lneas de
listAction(), pero no vale lo mismo que el test anterior. Lo que nos interesa es garantizar que
llamamos a findAll() dentro de nuestro cdigo y aqu no se comprueba.
En una metodologa TDD/BDD estricta no se debe dar el problema de que haya tests invlidos puesto que habremos de especificar primero en el test que findAll() debe ser llamado
$taskRepo->findAll()->shouldBeCalled(); y si no lo especificamos no debemos codificarlo en
nuestro cdigo de produccin.
Este libro trata sobre testing. Quizs se deje llevar hacia TDD BDD a hacia ninguno. Tu filosofa
de desarrollo la debes marcar t y sentirte cmodo con ella. No hay verdades absolutas.
He oido exigir al menos un 60% de cobertura para poder desplegar una aplicacin en produccin.
Esto qu aporta?. Eso quiere decir que es cdigo de ms calidad?. Ya hemos visto que en realidad
no tiene por qu ser as. Refactorizar varias veces es un paso fundamental que realmente aumenta
la calidad. La cobertura, si es con tests bien escritos, aumenta la seguridad.
Si se sigue con cuidado BDD y se dispone de experiencia, lograremos una cobertura de 100% con tests
precisos. Por otro lado esto implica estar muy entrenado en BDD y nos pagan por funcionalidades,
no por escribir tests.
Hay gente que prefiere escribir un 20% de los test antes de escribir el cdigo y el 80% o menos despus.
Por otro lado cuanto ms retrasemos el test ms retrasamos el feedback que obtenemos. Quizs no
sea apropiado perseguir el 100% de cobertura, pero deberamos tener unos buenos tests de aceptacin
que prueben bien la aplicacin al completo ya se hayan escrito antes o despus del cdigo.
Doubles
Todos los colaboradores que reemplazamos por objetos de mentira que insertamos para controlar
el SUT son dobles o Doubles o Test Doubles. Un Test Double es el nombre que damos a los objetos
que reemplazan a los colaboradores. Sirve tanto para un Mock que para un Stub, as que si tenemos
duda en una conversacin y no queremos hacer el ridculo, empleando la expresin Test Double
acertaremos seguro. De los diferentes tipo de Test Doubles que vamos a ver a continuacin, slo los
Mocks verifican el comportamiento. El resto de Doubles slo verifican el estado.
Dummy
Un objeto de tipo dummy es un objeto tonto, un objeto que no vamos a usar para nada pero que
necesitamos por ejemplo para poder satisfacer las necesidades de un constructor y que luego no
vamos a utilizar.
Ejemplo de un Dummy con Prophecy para ser insertado en la clase Markdown y cumplir con los
requerimientos del constructor:
1
2
3
16
<?php
$eventDispatcher = $this->prophesize('MarkdownEventEventDispatcher');
$markdown = new Markdown($eventDispatcher->reveal());
Fakes
Un objeto double de tipo Fake aade un poco ms de funcionalidad a los Dummy. Si intentamos
llamar a un mtodo de un objeto Dummy tendremos un error. Hay veces que tenemos que poder
llamar a un mtodo de un Dummy simplemente para que nuestro test continue y no tener un
error. Para esto estn los Fake que son Dummies con mtodos, pero ojo, estos mtodos no hacen
ni devuelven nada.
Ejemplo de un objeto Fake que como he dicho es como un Dummy, pero con algo ms de funcionalidad, en este caso se va a declarar un mtodo para que cuando se llame internamente no de error.
El mtodo puede ser llamado con un argumento de tipo 'Markdown\Event\EndOfLineListener':
1
2
3
4
5
<?php
$eventDispatcher = $this->prophesize('Markdown\Event\EventDispatcher');
$eventDispatcher->addListener(Argument::type('Markdown\Event\EndOfLineListen\
er'));
$markdown = new Markdown($eventDispatcher->reveal());
Stubs
En los Stubs lo importante es lo que devuelve la llamada a sus mtodos, son una forma de garantizar
la salida de los objetos colaboradores. Por ejemplo cuando llamo a $em->find(1) devolver un
objeto entidad. La finalidad de los Stubs es reemplazar una funcionalidad concreta del colaborador
para garantizar el funcionamiento del colaborador.
Con Mockery se creara de la siguiente forma:
1
2
3
<?php
$miMock = m::mock('MiClase');
$miMock->shouldReceive('readTemp')->andReturn(11);
Con PHPUnit:
1
2
3
4
17
<?php
$miMock = $this->getMock('MiClase');
$miMock->expects($this->any())->method('readTemp')->will($this->returnValue(\
11));
<?php
$miMock = $this->prophesize('MiClase');
$miMock->readTemp()->willReturn(11);
Mocks
Es un tipo de objeto Double sobre los que establecemos unas expectativas de uso y del que no nos
preocupa controlar lo que devuelve su llamada.
Por ejemplo: espero que el objeto colaborador $em->flush() sea llamado una sola vez dentro del
SUT, o que no sea llamado nunca, o al menos una vez, o que persist() sea llamada con un objeto
de tipo MiEntidad.
Con Mockery se creara de la siguiente forma:
1
2
3
<?php
$miMock = m::mock('MiClase');
$miMock->shouldReceive('readTemp')->times(3);
Con PHPUnit:
1
2
3
<?php
$miMock = $this->getMock('MiClase', array('readTemp', '', false);
$miMock->expects($this->exactly(3))
<?php
$miMock = $this->prophesize('MiClase');
$miMock->readTemp()->shouldBeCalledTimes(3);
18
19
1
2
3
4
5
<?php
$color1
$color2
$color1
$color1
De estos Value Object no necesitamos hacer Doubles puesto que los podemos crear sin ningn tipo
de complicacin ni sobrecoste y debemos tratarlos como primitivas del lenguaje.
Otro tipo de objetos colaboradores para los que no debemos hacer Test Doubles son los objetos que
no podemos modificar. Concretamente las libreras de terceros [Growing Object-Oriented Software
Guided by Tests, captulo 8]. El feedback que obtenemos de los tests no lo podemos trasladar a
un refactoring de la librera. Aunque dispongamos del cdigo fuente no es conveniente modificar
objetos de terceros. Adems no podemos tener total seguridad de que el Double que creemos imite
perfectamente el comportamiento de la librera incluso tras una actualizacin. Para integrar libreras
de terceros en nuestro cdigo debemos hacerlos mediante una capa de adaptacin e interfaces que
representen la relacin de nuestra lgica de negocio con el mundo exterior.
20
Libreras de Mocks
Ahora que ms o menos tenemos claro qu son los Mocks, Stubs y allegados hay que ver como los
creamos en nuestros tests. Para ello tenemos diversas posibilidades.
PHPUnit
La primera opcin es no usar ninguna librera, porque casi seguro que ya estaremos usando
PHPUnit para nuestros tests. A fin de cuentas es la referencia para el testing con PHP. Un Mock o
un Stub con PHPUnit se crea de la siguiente forma:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
$emMock = $this->getMock('\Doctrine\ORM\EntityManager',
array('getRepository', 'getClassMetadata', 'persist', 'flush'), array(), '',\
false);
$emMock->expects($this->any())
->method('getRepository')
->will($this->returnValue(new FakeRepository()));
$emMock->expects($this->any())
->method('getClassMetadata')
->will($this->returnValue((object)array('name' => 'aClass')));
$emMock->expects($this->any())
->method('persist')
->will($this->returnValue(null));
$emMock->expects($this->any())
->method('flush')
->will($this->returnValue(null));
Mockery
Mockery es posiblemente la librera ms usada en PHP para crear Mocks y Stubs. Realmente
simplifica mucho la tarea de escribir cdigo y lo hace mas legible. El mismo ejemplo de antes en
PHPUnit escrito con Mockery sera as:
http://phpunit.de/
https://github.com/padraic/mockery
1
2
3
4
5
6
7
8
21
<?php
$emMock = \Mockery::mock('\Doctrine\ORM\EntityManager',
array(
'getRepository' => new FakeRepository(),
'getClassMetadata' => (object)array('name' => 'aClass'),
'persist' => null,
'flush' => null,
));
Adems de ser muchsimo ms claro y cmodo, Mockery ofrece muchas otras facilidades.
Integrar Mockery en PHPUnit es muy sencillo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
use \Mockery as m;
class TemperatureTest extends PHPUnit_Framework_TestCase
{
public function tearDown()
{
m::close();
}
public function testGetsAverageTemperatureFromThreeServiceReadings()
{
$service = m::mock('service');
$service->shouldReceive('readTemp')->times(3)->andReturn(10, 12, 14);
$temperature = new Temperature($service);
$this->assertEquals(12, $temperature->average());
}
}
Prophecy
Es una librera de mocking bastante nueva creada expresamente para satisfacer las necesidades de
PHPSpec2. Respecto a Mockery notaremos que cambia la sintaxis que a mi entender es muy clara y
22
legible. Hay otras diferencias ms importantes respecto a Mockery, pero voy a reservar un apartado
entero para esto a continuacin.
Lo que nos debe quedar claro es que si nos decantamos por PHPSpec2, aunque podemos usar
Mockery u otra librera, Prophecy es la librera en la que se van a centrar los desarrolladores y
por lo tanto donde encontraremos ms soporte y garanta de continuidad.
AspectMock
Esta librera se ha creado dentro del framework de testing de Codeception. Se basa en las librera de
AOP GoAOP y podemos hacer cosas como crear doubles de mtodos estticos.
Un ejemplo de test con AspectMock usado dentro de PHPUnit:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
use AspectMock\Test as test;
class UserTest extends \PHPUnit_Framework_TestCase
{
protected function tearDown()
{
test::clean(); // remove all registered test doubles
}
public function testDoubleClass()
{
$user = test::double('demo\UserModel', ['save' => null]);
\demo\UserModel::tableName();
\demo\UserModel::tableName();
$user->verifyInvokedMultipleTimes('tableName',2);
}
}
La principal ventaja de AspectMock es que podemos emplearlo para hacer mocks de casi cualquier
cdigo PHP. No siempre nos enfrentamos con software bien estructurado y que haya sido creado
teniendo en cuenta el testing. Es posible que queremos hacer tests del algo que nunca escuch hablar
de inyeccin de dependencias.
https://github.com/phpspec/prophecy
https://github.com/lisachenko/go-aop-php
https://github.com/Codeception/AspectMock
23
<?php
interface User
{
public function getRating();
public function setRating($rating);
}
class UserRatingCalculator
{
public function increaseUserRating(User $user, $add = 1)
{
$user->setRating($user->getRating() + $add);
}
}
Es ms o menos una variacin del ejemplo de la calculadora. El test con Mockery sera algo as:
1
2
3
4
5
6
7
<?php
$user = Mockery::mock('User');
$user->shouldReceive('getRating')->andReturn(2);
$user->shouldReceive('setRating')->with(4)->once();
$calc = new UserRatingCalculator();
$calc->increaseUserRating($user->mock(), 2);
1
2
3
4
5
6
7
24
<?php
$user = $prophet->prophesize('User');
$user->getRating()->willReturn(2);
$user->setRating(4)->shouldBeCalled();
$calc = new UserRatingCalculator();
$calc->increaseUserRating($user->reveal(), 2);
Excepto por pequeas diferencias ambos tests parecen iguales. Compliquemos un poco ms las cosas.
Digamos que ahora queremos disparar un evento antes y despus de un cambio en el rating. La nueva
calculadora sera esta:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class UserRatingCalculator
{
private $dispatcher;
public function __construct(EventDispatcher $dispatcher)
{
$this->dispatcher = $dispatcher;
}
public function increaseUserRating(User $user, $add = 1)
{
$this->dispatcher->userRatingIncreasing($user->getRating());
$user->setRating($user->getRating() + $add);
$this->dispatcher->userRatingIncreased($user->getRating());
}
}
1
2
3
4
5
6
7
8
9
10
11
25
<?php
$user = Mockery::mock('User');
$user->shouldReceive('getRating')->andReturn(2, 2, 4);
$user->shouldReceive('setRating')->with(4)->once();
$disp = Mockery::mock('EventDispatcher');
$disp->shouldReceive('userRatingIncreasing')->with(2)->once();
$disp->shouldReceive('userRatingIncreased')->with(4)->once();
$calc = new UserRatingCalculator($disp->mock());
$calc->increaseUserRating($user->mock(), 2);
La clave est en fijarse cmo se ha hecho el stub del mtodo getRating() con tres valores de retorno
consecutivos. A esto de le llama structure binding, fijacin a la estructura del cdigo, significa
que los tests son dependientes de como el cdigo est escrito (estructurado), hay tres llamadas
consecutivas en ese orden para devolver esos valores.
Con Prophecy la solucin es diferente:
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$user = $prophet->prophesize('User');
$user->getRating()->willReturn(2);
$user->setRating(Argument::type('integer'))->will(function($rating) {
$this->getRating()->willReturn($rating);
})->shouldBeCalled();
$disp = $prophet->prophesize('EventDispatcher');
$disp->userRatingIncreasing(2)->shouldBeCalled();
$disp->userRatingIncreased(4)->shouldBeCalled();
$calc = new UserRatingCalculator($disp->reveal());
$calc->increaseUserRating($user->reveal(), 2);
Prophecy usa un enfoque tipo message binding (orientado el mensaje), que significa que el
comportamiento del mtodo no cambia en el tiempo, sino es cambiado por el otro mtodo.
Cul es la diferencia real entre el enfoque de ambas libreras? Consideremos un cambio en la
calculadora:
1
2
3
4
5
6
7
8
9
10
11
26
<?php
public function increaseUserRating(User $user, $add = 1)
{
$initialRating = $user->getRating();
$this->dispatcher->userRatingIncreasing($initialRating);
$user->setRating($initialRating + $add);
$resultingRating = $user->getRating();
$this->dispatcher->userRatingIncreased($resultingRating);
}
Simplemente hemos puesto el rating inicial en una variable local $initialRating por clarificar el
cdigo. El problema es que este pequeo cambio hace que se rompa el test hecho con Mockery
puesto que ahora hay slo dos llamadas a getRating() en vez de tres. Si los test estn asociados a
la estructura y esta cambia entonces el test falla.
En el caso de Prophecy el test inicial sigue pasando porque el test se ha creado centrado en los
mensajes que se pasan entre los objetos y esto no ha cambiado.
La diferencia conceptual no esta en cmo escribir los tests, sino cundo hay que corregirlos. Mockery
te puede poner en la situacin en la que tengas que rehacer los tests simplemente porque has hecho
un refactoring que afecta a la estructura. Prophecy postula que en este caso el test no debera fallar
porque el comportamiento de los objetos sigue siendo el mismo.
27
Este rbol refleja lo que lo elegira, es posible que haya gente que no empleara Prophecy por ser muy
nueva y porque Mockery tiene mucha comunidad y es muy sencilla de integrar en casi cualquier
contexto de testing. Lo que tengo claro es que no empleara el sistema de Mocking de PHPUnit
porque hay que escribir demasiado cdigo y es difcil de leer.
<?php
class Coche {
private $motor;
public function __construct() {
$this->motor = new Motor();
}
public function avanzar($velocidad, $tiempo=1) {
$this->motor->encender();
// hacer otras cosas
}
}
29
En esta clase Coche no podemos cambiar el tipo de Motor por otro. En nuestros tests tampoco
podremos reemplazar ese Motor por un Mock de ste para esegurarnos que llamar a avanzar()
enciende el Motoruna vez.
En el ejemplo anterior si aplicamos inyeccin de dependencias:
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class Coche {
public $motor;
public function __construct($motor) {
$this->motor = $motor;
}
public function avanzar($velocidad) {
$this->motor->encender();
// hacer otras cosas
}
}
Simplemente pasamos Motor como un parmetro del constructor. Ahora podemos reemplazar
nuestro Motor por un MotorDeAvion
1
2
3
<?php
$coche1 = new Coche(new Motor());
$coche2 = new Coche(new MotorDeAvion());
<?php
use \Mockery as m;
class CocheTest extends PHPUnit_Framework_TestCase
{
public function testCocheArrancaMotor()
{
$motorMock = m::mock('Motor');
$motorMock->shouldReceive('encender')->once();
$coche = new Coche($motorMock);
}
14
15
16
17
18
30
Si no podemos sustituir el Motor por un Test Double obtenemos un error. Este es uno de los motivos
por los que podemos decir que escribir test hace que nuestro cdigo mejore en calidad.
No por ver un operador new tenemos que afirmar ese cdigo es malo, hay casos en los que no es una
mala prctica.
Los Value Object (objetos simples que no tienen un atributo de identidad como por ejemplo
instancias de CodigoPostal) no hay que inyectarlos y los podemos crear con new. Estos Value Object
son parecidos a los tipos primitivos del lenguaje y no tienen a su vez dependencias con objetos ms
complejos que ellos (otros Value Objects).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class CodigoPostal {
private $cp;
public function __construct($cp) {
$this->cp = $cp;
}
// getter y setter
}
class MiController {
public function newCodigoPostalAction($cp) {
$cpObject = new CodigoPostal($cp);
// ... no pasa nada por ese 'new'
}
}
Los Data Transfer Objects (DTO), de alguna forma son un wrapper o un facade de una base de datos
del algn tipo. Los DTO tienen identidad. La finalidad de los DTO es facilitar el flujo de informacin
entre una capa de dominio y otra. No tienen dependencias, pero pueden tener asociaciones. Por
ejemplo, a un array o a una coleccin. Son objetos de transicin que de alguna forma deben ser
persistidos o serializados.
Es complicado insertar un objeto como inyeccin de dependencias que previamente no debera
existir.
Un ejemplo de estos objetos DTO son las entidades de Doctrine o los objetos de eventos tipo Event
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
31
<?php
class UserService {
private $reposotory;
private $eDispatcher;
public function __construct(EventDispatcher $eDispatcher, UserRepository $repos\
itory)
{
$this->eDispatcher = $eDispatcher;
$this->repository = $repository;
}
public function addUser() {
// ... no pasa nada por este 'new'
$event = new UserEvent($user)
$this->eDispatcher->dispatch('user.new', $event);
};
}
En determinadas situaciones podemos crear los DTO mediante factorias (Factory Pattern) en cuyo
caso, las factoras debern ser insertadas mediante injeccin de dependencias.
<?php
$coche->motor->getTurbo()->encender()
<?php
$coche->motor->arrancar()
Nosotros no debemos conocer si estamos trabajando con un motor que posee un Turbo y cmo se
maneja. Slo queremos arrancar el coche.
Tambin es ms complicado escribir los test si no se sigue esta regla:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
32
<?php
use \Mockery as m;
class CocheTest extends PHPUnit_Framework_TestCase
{
public function testCocheArrancaMotor()
{
$turboMock = m::mock('Turbo');
$turboMock->shouldReceive('encender')->once();
$motor = m::mock('Motor');
$motor->shouldReceive('getTurbo')->andReturn($turboMock);
$coche = new Coche($motor);
}
protected function tearDown()
{
m::close();
}
}
Imaginemos que nuestro controlador trabaja con un objeto Coche, nosotros slo sabemos de Coche
a travs de sus mtodos pero tenemos o no debemos tener conocimiento de los elementos que lo
componen.
En Symfony es comn encadenar mtodos de esta forma. Un ejemplo de la documentacin oficial:
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
use Acme\StoreBundle\Entity\Product;
use Symfony\Component\HttpFoundation\Response;
//...
public function saveAction($product)
{
$em = $this->getDoctrine()->getManager();
$em->persist($product);
$em->flush();
return new Response('Created product id '.$product->getId());
}
33
<?php
use Acme\StoreBundle\Entity\Product;
use Symfony\Component\HttpFoundation\Response;
//...
public function saveAction($product)
{
$this->productService->save($product);
return new Response('Created product id '.$product->getId());
}
Uso de Singleton
El patrn Singleton tiene la distincin de ser no solamente uno de los patrones ms sencillos, sino
tambin uno de los ms controvertidos. El principal problema con este patrn es que se introduce
un estado global en nuestro programa. Todo el mundo entiende que no hay que emplear variables
globales, pero muchas veces se sustituyen por Singletons que hacen la misma funcin.
Otro gran problema con los Singletons es que puede ocultar dependencias entre mdulos.
Por ejemplo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class S { //Singleton
private static $instance;
public static function getInstance() {};
public function getValue() {};
public function setValue() {};
}
class A {
private $s;
public function __construct(S $mySingleton) {
$this->s = $mysingleton;
}
public function unaFuncion() {
// ...
if ($this->s->getInstance()->getValue())
// ....
19
20
21
22
23
24
25
26
27
28
29
30
31
32
34
}
}
class B {
private $s;
public function __construct(S $mySingleton) {
$this->s = $mysingleton;
}
public function otraFuncion() {
$z = ...
$this->s->getInstance()->setValue($z);
}
}
Las clases A y B tienen una dependencia entre s que no puede ser percibida a simple vista mirando
las interfaces de las clases.
<?php
namespace Symfony\Component\DependencyInjection;
/**
* A simple implementation of ContainerAwareInterface.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @api
*/
abstract class ContainerAware implements ContainerAwareInterface
{
/**
* @var ContainerInterface
*
* @api
*/
18
19
20
21
22
23
24
25
26
27
28
29
30
31
35
protected $container;
/**
* Sets the Container associated with this Controller.
*
* @param ContainerInterface $container A ContainerInterface instance
*
* @api
*/
public function setContainer(ContainerInterface $container = null)
{
$this->container = $container;
}
}
Es bastante comn ver esto, por ejemplo en los controladores base de Symfony2.
1
2
3
4
5
<?php
// en Symfony / Bundle / FrameworkBundle / Controller / Controller.php
// ...
class Controller extends ContainerAware
// ...
Poner el Container al completo dentro una clase es tan malo como trabajar con Singleton
Esta prctica de inyectar el Container hace que el cdigo sea difcil de testear. Para hacer el testing
podemos:
Inyectar un Test Double del DIC y devolver Doubles de los servicios. Es decir, inyectar un
Double del DIC que hace casi lo mismo que el propio DIC pero que responde con Doubles de
servicios. Esto al final es hacer el cdigo de testing ms farragoso y aadir sobrecarga para
nada.
Usar el DIC pero hacer que este devuelva Doubles del los servicios. Implica cambiar la
configuracion del DIC y en realidad estamos haciendo tests de integracin en vez de tests
unitarios.
Hacer un Double de la clase a testear para sobreescribir un mtodo, normalmente get(),
que es el que se suele usar para acceder a los servicios del contenedor. Esto es una mala
prctica. Sobreescribir un mtodo de la propia clase a testear es ya en si un bad smell porque
no puedes asegurar que la versin con la que sobreescribes tenga el mismo comportamiento
que la versin original.
Por otro lado, el uso directo del contenedor de dependencias nos oculta las dependencias de las que
hace uso la clase puesto que la nica dependencia aparente es el propio contenedor.
36
Demasiadas dependencias
Cuando una clase tiene demasiadas dependencias, digamos unas 5 6, aunque no se puede poner un
nmero exacto, es posible que tengamos que refactorizar puesto que seguramente est muy acoplada
entre diferentes objetos y su propsito seguramente sea demasiado amplio.
La solucin a este bad smell no es ocultar las dependencias agrupdola en otra clase o pasando el
Container. La solucin es repensar su funcionalidad y en todo caso si algunas de esas dependencias
se pueden agrupar como un servicio.
<?php
class UserService {
private $mailer;
private $reposotory;
public function __construct(Mailer $mailer, UserRepository $repository) {
$this->mailer = $mailer;
$this->repository = $repository;
}
public function addUser() {};
public function emailNewUser(){
// envia un mail al usuario cada vez que se crea uno
};
}
37
Este servicio se describe como Servicio que crea un usuario y enva el email de confirmacin. La
conjuncin y de la frase ya nos da la idea de que est haciendo ms cosas de las que debera.
Una forma de solucionar esto es empleando eventos:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class UserService {
private $reposotory;
private $eDispatcher;
public function __construct(EventDispatcher $eDispatcher, UserRepository $repos\
itory)
{
$this->eDispatcher = $eDispatcher;
$this->repository = $repository;
}
public function addUser() {
// ...
$event = new UserEvent($user)
$this->eDispatcher->dispatch('user.new', $event);
};
}
Ahora UserService slo se encarga de crear usuarios. Enviar los emails es cosa de la clase que
gestione ese evento. As podemos extender ms el cdigo, por ejemplo, escribiendo un log cada vez
que se cree un usuario sin tener que modificar el servicio.
Principios SOLID
Principio de Responsabilidad nica (Single Responsibility
Principle -SRP- )
Una clase no debe tener ms de una razn para cambiar
Las clases deben ser diseadas para tener una nica rea de ocupacin y un conjunto de operaciones
que definan e implementen esa ocupacin en particular un nada ms.
Este punto ya lo hemos visto cuando hablbamos en los Bad Smells sobre clases con demasiadas
responsabilidades.
38
<?php
class Circulo {
public $radio;
public $centro;
}
class Cuadrado {
public $lado;
public $puntoSuperiorIzquierda;
}
class PaintGUI {
public function dibujarCirculo(Circulo cir) {}
public function dibujarCuadrado(Cuadrado cua) {}
}
Si ahora aadimos un nuevo tipo de figura Triangulo debemos modificar la clase PaintGUI, sera
mucho ms efectivo si hubisemos creado una interfaz para las figuras con un mtodo comn.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
interface FiguraInterface {
public function dibujar();
}
class Circulo implements FiguraInterface {
public $radio;
public $centro;
public function dibujar(){}
}
class Cuadrado implements FiguraInterface {
public $lado;
public $puntoSuperiorIzquierda;
15
16
17
18
19
20
21
22
39
De esta forma no hay que modificar PaintGUI, nuestra clase esta abierta para ser extendida y cerrada
para ser modificada.
<?php
class Vehicle {
function startEngine() {
// Default engine start functionality
}
function accelerate() {
// Default acceleration functionality
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
40
<?php
class Car extends Vehicle {
function startEngine() {
$this->engageIgnition();
parent::startEngine();
}
private function engageIgnition() {
// ...
}
}
class ElectricBus extends Vehicle {
function accelerate() {
$this->increaseVoltage();
$this->connectIndividualEngines();
}
private function increaseVoltage() {
// ...
}
private function connectIndividualEngines() {
// ...
}
}
Una clase cliente debera poder hacer uso de Car o ElectricBus como si de un Vehicle se tratase.
1
2
3
4
5
6
7
<?php
class Driver {
function go(Vehicle $v) {
$v->startEngine();
$v->accelerate();
}
}
Lo requerimientos que deben cumplir los mtodos que sobreescriban los de la clase padre son:
Su signatura debe encajar con la del mtodo padre
41
42
No podemos llevarnos a otro sitio la clase Foo sin llevarnos con ella la clase Bar. Para desacoplar
estas clases podemos introducir la interfaz IBar de forma que Foo dependa de la ella y que Bar la
implemente.
Introducir IBar es un cambio necesario pero no suficiente. Hemos extraido la interfaz desde Bar,
pero esta perspectiva es incorrecta. Debemos extraer la interfaz que implementar Bar a partir de
los requerimientos de Foo de forma que podamos hacer un paquete con Foo e IBar
Injeccin Vs Inversin
No hay que confundir inyeccin de dependencias e inversin de dependencias, a pesar de que suelen
compartir el mismo acrnimo DI (Dependency Inversion/Dependency Injection).
43
La injeccin trata de proveer las dependecias que necesita una clase en su contructor, setter o
atributos, para evitar que la clase tenga que crear estos objetos. Esto tiene mucho que ver con el
uso del operador new.
La inyeccin de dependencias tiene nada que ver con el acoplamiento de las clases. Podemos inyectar
dependencias sin que se reduzca el acoplamiento.
Un ejemplo:
1
2
3
4
5
6
7
8
<?php
class Foo {
private $bar;
public function __construct(Bar $bar) {
$this->bar = $bar;
}
}
Aqu estamos inyectando Bar pero la clase Foo sigue dependiendo de la implementacin concreta
de Bar.
La inversin de dependencias trata sobre cmo Foo establece un contrato tipo IBar que marca la
implementacin queBar debe completar. Ojo, no es Bar el que crea un contrato IBar que Foo puede
usar.
1
2
3
4
5
6
7
8
9
<?php
class Foo {
private $bar;
public function __construct(IBar $bar) {
$this->bar = $bar;
}
}
Normalmente inyeccin e inversin de dependencias van de la mano porque de esta forma es sencillo
reemplazar las implementaciones.
44
Tipos de inyeccin
Existen diferentes formas en las que las dependencias pueden ser inyectadas.
En el constructor es la forma ms normal y la recomendada.
1
2
3
4
5
6
7
8
9
10
11
12
<?php
class NewsletterManager
{
protected $mailer;
public function __construct(\Mailer $mailer)
{
$this->mailer = $mailer;
}
// ...
}
Podemos indicar el tipo \Mailer para el parmetro de forma que obtengamos un error claro si se
inyecta otra cosa
La inyeccin en el constructor tiene ventajas:
Si la dependencia es un requerimiento nos aseguramos de que est presente si no el objeto no
se puede construir.
El constructor slo se llama cuando se crea el objeto por lo que podemos estar seguros de que
la dependencia no se cambia durante su vida.
Entre los inconvenientes se encuentra:
No es apropiado para trabajar con dependencias opcionales.
Es difcil trabajar con jerarquas de clases al tener que extender y sobreescribir el constructor
si fuese necesario.
Podemos inyectar las dependencias mediantes mtodos setter con la dependencia como parmetro:
1
2
3
4
5
6
7
8
9
10
11
12
45
<?php
class NewsletterManager
{
protected $mailer;
public function setMailer(\Mailer $mailer)
{
$this->mailer = $mailer;
}
// ...
}
<?php
$objeto = new UnaClase(new NewsletterManager());
1
2
3
4
5
6
7
8
46
<?php
class UnaClase {
private $newsManager;
public function __construct($newsManager){
$this->newsManager = $newsManager;
$this->newsManager->setMailer($otraCosa)
}
}
Estaremos rompiendo la encapsulacin, puesto que nuestro controlador no debera saber mas que de
sus propias dependencias y ahora tiene informacin sobre cmo esta compuesto el NewsletterManager
que emplea. Ahora no se puede reescribir NewsletterManagersin cambiar el cdigo de UnaClase.
Otra razn para no emplear la inyeccin en el setter es que podemos tener objetos perfectamente
construidos pero que no tengan disponibles todas las dependencias que necesitan.
Un ejemplo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
class Foo {
private $dep;
public function setDependency(SomeDependency $dep) {
$this->dep = $dep;
}
public function someMethod() {
$baz = $this->dep->xx();
}
}
class Bar {
private $foo;
public function __construct(Foo $foo) {
$this->foo = $foo;
$this->foo->someMethod();
}
}
26
27
47
Aqu se producir un error porque Bar no tiene forma de saber si se ha llamado a setDependency()
de Foo.
Se puede dar el caso de que un test unitario que tenga un Mock de Foo->someMethod() funcione
perfectamente y falle al pasarlo a produccin, lo que nos puede llevar a errores difciles de trazar.
El caso que puede justificar la inyeccin de dependencias en setter es cuando stas son opcionales y
el objeto no las necesita para cumplir su responsabilidad:
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class Foo {
private $logger;
public function setLogger(Logger $logger) {
$this->logger = $logger;
}
public function someMethod() {
if ($this->logger) $this->logger->log('someMethod called');
}
}
Cuando Bar llame a $foo->someMethod() no ocurrir nada si no hay un logger. Como mucho se
producir algo de confusin al echar de menos los logs.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
48
<?php
// src/Acme/HelloBundle/Controller/HelloController.php
namespace Acme\HelloBundle\Controller;
use Symfony\Component\HttpFoundation\Response;
class HelloController
{
public function indexAction($name)
{
return new Response('<html><body>Hello '.$name.'!</body></html>');
}
}
# src/Acme/HelloBundle/Resources/config/services.yml
parameters:
# ...
acme.controller.hello.class: Acme\HelloBundle\Controller\HelloController
services:
acme.hello.controller:
class: "%acme.controller.hello.class%"
Si un controlador est definido como un servicio tenemos que referirnos a l con una sintaxis
diferente:
1
2
<?php
$this->forward('acme.hello.controller:indexAction');
app/config/routing.yml
hello:
path:
/hello
defaults: { _controller: acme.hello.controller:indexAction }
El hecho de tener que emplear una sintaxis diferente si nuestro controlador es un servicio o no es
una desventaja y un problema, entiendo que esto debera ser totalmente transparente. Adems hace
que sea ms difcil de configurar.
Hay un ticket de Symfony relativo al tema de los controladores como servicio, en l, Fabien
Potencier (el padre de Symfony) alega que los controladores no deben ser un servicio puesto que
https://github.com/symfony/symfony-docs/issues/457
49
el DIC (Dependency Injection Container) se debe emplear para manejar objetos globales y los
controladores no encajaran ah, son el pegamento entre la capa de Vista y Modelo y deberan ser lo
ms ligeros posible.
JMSDiExtraBundle
Existe un bundle JMSSiExtraBundle, que aporta una solucin, en mi opinin perfecta, entre
declarar el controlador como servicio e inyectar el contenedor.
JMSSiExtraBundle permite inyectar dependencias mediante anotaciones y permite inyectarlas en
los controladores sin necesidad de declararlos como servicios, de forma que no nos vemos obligados
a cambiar la sintaxis.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
use JMS\DiExtraBundle\Annotation as DI;
class Controller
{
private $em;
private $session;
/**
* @DI\InjectParams({
*
"em" = @DI\Inject("doctrine.orm.entity_manager"),
*
"session" = @DI\Inject("session")
* })
*/
public function __construct($em, $session)
{
$this->em = $em;
$this->session = $session;
}
// ... some actions
}
Por otro lado, en el mismo hilo tambin se indica que los controladores se deberan testear
funcionalmente y no de forma unitaria, de forma que el Mocking del DIC ya no es un problema
Mi recomendacin es declarar todos los servicios en el fichero services.yml a excepcin de los
controladores, a los que les injectaremos sus dependencias mediante anotaciones gracias a este
bundle.
http://jmsyst.com/bundles/JMSDiExtraBundle
50
Una nota: la injeccin en el constructor no es posible cuando hereda de una clase padre que tamben
tiene definido un constructor con injeccin de dependencias.
6
11
565
230
335
131
89
9
2
0
0
42
(40.71%)
(59.29%)
(23.19%)
(67.94%)
(0.00%)
(32.06%)
0.02
1.08
0
0 (0.00%)
0 (0.00%)
0 (0.00%)
38
51
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
Non-Static
Static
Method Calls
Non-Static
Static
Structure
Namespaces
Interfaces
Traits
Classes
Abstract Classes
Concrete Classes
Methods
Scope
Non-Static Methods
Static Methods
Visibility
Public Method
Non-Public Methods
Functions
Named Functions
Anonymous Functions
Constants
Global Constants
Class Constants
38
0
52
50
2
(100.00%)
(0.00%)
(96.15%)
(3.85%)
7
2
0
9
0 (0.00%)
9 (100.00%)
39
39 (100.00%)
0 (0.00%)
37
2
0
0
0
0
0
0
(94.87%)
(5.13%)
(0.00%)
(0.00%)
(0.00%)
(0.00%)
Complejidad ciclomtica
La complejidad ciclomtica (Cyclomatic Complexity -CC- ) indica el nmero de puntos en el cdigo
donde se toman decisiones.
Por ejemplo:
1
2
3
4
5
6
7
8
9
<?php
// ....
public function hacerAlgo() {
// Uno
$i = $i+2;
if($i == TRUE) // Dos
{}
10
11
12
52
1 - 4: Poca Complejidad
5 - 7: Complejidad moderada
8 - 10: Complejidad alta
11+: Complejidad muy alta
1 - 2: Poca Complejidad
3 - 4: Complejidad Moderada
5 - 6: Complejidad Alta
6+: Complejidad muy alta
Tambin se puede obtener la CC por lnea de cdigo, en cuyo caso aplicamos la siguiente tabla:
NPATH
NPATH es una mtrica de software que nos indica en nmero de posibles caminos que puede seguir
un mtodo.
En el ejemplo anterior:
1
2
3
4
5
6
7
8
9
10
11
53
<?php
// ....
public function hacerAlgo() {
$i = $i+2;
if($i == TRUE)
{}
foreach($i as $algo)
{}
}
El valor mximo de NPATH para el mtodo es 8, que se calcula de NPATH = 2(CC - 1).
En funcin del valor de NPATH y la siguiente tabla podemos clasificar la complejidad en:
El valor de NPATH es importante porque no indica el nmero mnimo de tests que debemos crear
para probar completamente un mtodo. En el ejemplo anterior deberamos crear 8 tests unitarios para
asegurarnos de probar completamente la funcion.
CRAP
C.R.A.P. (Change Risk Analysis and Predictions) es una mtrica de software creada por Alberto
Savoia diseada para analizar la cantidad de esfuerzo que hay que invertir en mantener un trozo
de cdigo.
La frmulade CRAP es:
1
Donde comp es la complejidad ciclomtica y cov el porcentaje del cdigo cubierto por tests.
Si la cobertura es del 100% la frmula se reduce a:
http://www.artima.com/weblogs/viewpost.jsp?thread=210575
54
C.R.A.P.(m) = comp(m)
En este caso el riesgo de cambiar algo es directamente proporcional a la complejidad del cdigo.
Si no hay ningn tipo de test la frmula es:
1
< 5 Magnfico
5-15 Aceptable
15-30 bueno, hay que revisarlo
30+ es basura
CRAP
https://github.com/sebastianbergmann/php-code-coverage
55
PDepend
PDepend es otra herramienta de anlisis de cdigo esttico un poco ms compleja que phploc.
Calcula mtricas de software y genera grficas de dependencia y mtricas que son muy tiles para
obtener una visin general y rpida de nuestro proyecto.
Un ejemplo de grfico de dependencias:
Grfico de dependencias
56
Grfica de pirmide
DataFixutures
Una de las cosas que seguro vamos a tener que hacer en el testing de nuestra aplicacin es crear un
conjunto de datos de prueba. Esto tambin debemos hacerlo de manera automatizada.
Los datos de prueba se emplearn en los tests funcionales y en el StoryBDD pero nunca en los tests
unitarios. Hay que recordar que los tests unitarios no interacionan con las base de datos.
<?php
// app/AppKernel.php
// ...
class AppKernel extends Kernel
{
// ...
public function registerContainerConfiguration(LoaderInterface $loader)
{
$loader->load(__DIR__.'/config/config_'.$this->getEnvironment().'.yml');
}
}
http://symfony.com/doc/current/cookbook/configuration/environments.html
DataFixutures
58
Podemos cambiar el fichero config_test.yml para usar una base de datos diferente usando un
parameters_test.yml personalizado:
1
2
3
4
#config_test.yml
imports:
- { resource: config_dev.yml }
- { resource: parameters_test.yml }
Para llamar a la aplicacin desde el navegador usando el entorno de test debemos usar el fichero
app_test.php. Es decir: http://localhost/nuestrapp/app_test.php
Es interesante tener un entorno de test especfico separado del de desarrollo. As podemos tener
parmetros parecidos a los de produccin pero con un conjunto de datos controlado.
DoctrineFixturesBundle
Si empleas Doctrine, el bundle DoctrinefixturesBundle facilita la tarea de carga de datos.
Los datos generados se pueden emplear para el entorno de test o para cargar los datos iniciales de
la aplicacin. Si lo usas para la carga inicial de datos hay que tener mucho cuidado, ya que antes de
cargar los datos borra la base de datos al completo si no se usa el flag append.
Para instalar el bundle hay que aadir a composer.json:
1
2
3
4
5
{
"require": {
"doctrine/doctrine-fixtures-bundle": "2.2.*"
}
}
DataFixutures
1
2
3
4
5
6
7
8
9
10
11
59
<?php
// ...
public function registerBundles()
{
$bundles = array(
// ...
new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle(),
// ...
);
// ...
}
Si tenemos claro que este bundle slo lo vamos a emplear en los entornos de dev y test pero no
en prod lo registraremos en AppKernel.php slo para eso entornos y as nos ahorramos cargarlo en
produccin.
1
2
3
4
5
6
7
<?php
if (in_array($this->getEnvironment(), array('dev', 'test'))) {
$bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
$bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle();
$bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle();
$bundles[] = new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle();
}
<?php
# Test/MyBundle/DataFixtures/ORM/LoadUserData.php
namespace Test3\MyBundle\DataFixtures\ORM;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Test\MyBundle\Entity\User;
class LoadUserData extends AbstractFixture implements OrderedFixtureInterface
{
/**
* Main method for fixtures insertion
DataFixutures
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
60
*
* @param Doctrine\Manager $manager
*/
public function load($manager)
{
$user = new User();
$manager->persist($user);
$manager->flush();
//Associate a reference for other fixtures
$this->addReference('user-admin', $user);
}
/**
* Get the order of this execution
*
* @return int
*/
public function getOrder()
{
return 1;
}
}
El mtodo getOrder() devuelve un nmero que indica en qu orden ejecutar las clases de inserccin
de datos.
La carga de datos se ejecuta mediante el comando:
1
./app/console doctrine:fixtures:load
app/console doctrine:mongodb:fixtures:load
Cuando trabajamos con MongoDB las clases de los fixtures se guardan en la carpeta DataFixtures/MongoDB.
DataFixutures
61
Es sencillo compartir objetos entre las clases que definimos. Para guardarlo llamamos a $this->addReference('mi-r
$miObjeto); y para recuperarlo en cualquier otra clase de fixtures a $this->getReference('mi-referencia').
Si necesitamos hacer cosas complejas, por ejemplo, usando el contenedor de dependencias, podemos
obtenerlo en la clase de fixtures. Para ello tienes que implementar la interfaz ContainerAwareInterface
y el mtodo setContainer.
Veamos un ejemplo para crear las contraseas de los usuarios:
// src/Acme/HelloBundle/DataFixtures/ORM/LoadUserData.php
namespace AcmeHelloBundleDataFixturesORM;
use DoctrineCommonDataFixturesFixtureInterface; use DoctrineCommonPersistenceObjectManager; use SymfonyComponentDependencyInjectionContainerAwareInterface; use SymfonyComponentDependencyInjectionContainerInterface; use AcmeHelloBundleEntityUser;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
class LoadUserData implements FixtureInterface, ContainerAwareInterface
{
/**
* @var ContainerInterface
*/
private $container;
/**
* {@inheritDoc}
*/
public function setContainer(ContainerInterface $container = null)
{
$this->container = $container;
}
/**
* {@inheritDoc}
*/
public function load(ObjectManager $manager)
{
$user = new User();
$user->setUsername('admin');
$user->setSalt(md5(uniqid()));
$encoder = $this->container
->get('security.encoder_factory')
->getEncoder($user)
DataFixutures
29
30
31
32
33
34
35
;
$user->setPassword($encoder->encodePassword('secret', $user->getSalt()));
$manager->persist($user);
$manager->flush();
}
}
Faker
Faker es una librera de Francois Zaninotto que nos ayuda a crear datos de prueba.
Instalar esta librera es muy sencillo con Composer:
1
2
3
4
#composer.json
"require-dev": {
"fzaninotto/faker": "dev-master"
}
62
DataFixutures
19
20
21
22
23
24
25
63
t aspernatur
// voluptatem sit aliquam. Dolores voluptatum est.
// Aut molestias et maxime. Fugit autem facilis quos vero. Eius quibusdam poss\
imus est.
// Ea quaerat et quisquam. Deleniti sunt quam. Adipisci consequatur id in occa\
ecati.
// Et sint et. Ut ducimus quod nemo ab voluptatum.
<?php
// unique() forces providers to return unique values
$values = array();
for ($i=0; $i < 10; $i++) {
// get a random digit, but always a new one, to avoid duplicates
$values []= $faker->unique()->randomDigit;
}
print_r($values); // [4, 1, 8, 5, 0, 2, 6, 9, 7, 3]
Ejemplo de optional():
1
2
3
4
5
6
7
8
<?php
// optional() sometimes bypasses the provider to return null instead
$values = array();
for ($i=0; $i < 10; $i++) {
// get a random digit, but also null sometimes
$values []= $faker->optional()->randomDigit;
}
print_r($values); // [1, 4, null, 9, 5, null, null, 4, 6, null]
DataFixutures
1
2
3
4
5
6
7
64
<?php
// optional() accepts a weight argument to specify the probability of receiving \
a NULL value.
// 0 will always return NULL; 1 will always return the provider. Default weight \
is 0.5.
$faker->optional($weight = 0.1)->randomDigit; // 90% chance of NULL
$faker->optional($weight = 0.9)->randomDigit; // 10% chance of NULL
Por defecto los valores generados por Faker estn en ingls, pero se pueden generar en espaol o en
otros idiomas.
1
2
<?php
$faker = Faker\Factory::create('es_ES');
Faker genera datos aleatorios, pero puede que nos interese generar siempre el mismo conjunto de
datos. Para hacer esto debemos inicializar Faker con la misma semilla:
1
2
3
<?php
$faker = Faker\Factory::create();
$faker->seed(1234);
De esta forma sucesivas invocaciones al script de Faker generarn los mismo resultados.
Faker tiene la posibilidad de crear entidades directamente con Doctrine pero lo ideal es emplearlo
dentro de DoctrineFixturesBundle.
Alice
Alice es una librera que permite crear fixtures de una forma bastante expresiva y sencilla mediante
archivos yaml.
Para instalarlo lo hacemos con Composer:
1
2
3
4
#composer.json
"require-dev": {
"nelmio/alice": "*"
}
DataFixutures
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
65
<?php
class LoadAdvisesData implements FixtureInterface
{
/**
* {@inheritDoc}
*/
public function load(ObjectManager $manager)
{
$loader = new \Nelmio\Alice\Loader\Yaml();
$objects = $loader->load(__DIR__.'/fixtures.yml');
$persister = new \Nelmio\Alice\ORM\Doctrine($manager);
$persister->persist($objects);
}
}
La definicin de los objetos de las entidades se hacen en fixtures.yml. Para un ejemplo sencillo se
crean todos los objetos dentro del mismo fichero, pero podramos partirlo en varios ficheros.
Los ficheros yaml de fixtures tienen esta forma:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Nelmio\Entity\User:
user0:
username: bob
fullname: Bob
birthDate: 1980-10-10
email: bob@example.org
favoriteNumber: 42
user1:
username: alice
fullname: Alice
birthDate: 1978-07-12
email: alice@example.org
favoriteNumber: 27
Nelmio\Entity\Group:
group1:
name: Admins
DataFixutures
1
2
3
4
5
6
7
66
Nelmio\Entity\User:
user{1..10}:
username: bob
fullname: Bob
birthDate: 1980-10-10
email: bob@example.org
favoriteNumber: 42
De esta forma se crean las entidades user1, user2 , user10, pero todos con los mismos valores.
Alice se combina con Faker. No hay que hacer nada a nivel de configuracin. Llamando, por ejemplo,
a <username()> estamos llamando en realidad a $faker->username
El fichero anterior para la creacin de usuarios quedara de esta forma:
1
2
3
4
5
6
7
Nelmio\Entity\User:
user{1..10}:
username: <username()>
fullname: <firstName()> <lastName()>
birthDate: <date()>
email: <email()>
favoriteNumber: <numberBetween(1, 200)>
Se puede llamar a mtodos de la entidad, incluido el constructor. Para ello especificaremos los
parmetros entre corchetes.
Para llamar el mtodo setLocation():
1
2
3
4
Nelmio\Entity\User:
user1:
username: <username()>
setLocation: [40.689269, -74.044737]
Nelmio\Entity\User:
user1:
__construct: [<username()>]
Se puede establecer que ciertos datos son opcionales mediante la notacin 50%? value : empty
value donde 50%es la probabilidad. Si null es un valor vlido entonces se puede resumir como %50?
value.
Un ejemplo:
DataFixutures
1
2
3
4
5
6
7
67
Nelmio\Entity\User:
user{1..10}:
username: <username()>
fullname: <firstName()> <lastName()>
birthDate: <date()>
email: <email()>
favoriteNumber: 50%? <numberBetween(1, 200)>
Nelmio\Entity\User:
# ...
Nelmio\Entity\Group:
group1:
name: Admins
owner: @user1
Se pueden establecer referencias a mltiples objetos, referencias opcional etc. Lo mejor es revisar la
documentacin de Alice.
En resumen con Alice podemos crear de una forma muy sencilla y legible los fixtures que
necesitemos en el 90% de los casos. Un ejemplo en el que no podramos, o sera difcil emplear
Alice, es que tengamos que hacer uso del Container de Symfony, como cuando queremos usar el
servicio de encoder para las passwords.
Como buena prctica empleamos Alice dentro de DoctrineFixturesBundle de forma que tenemos lo
mejor entre la flexibilidad de escribir los fixtures usando PHP y hacerlos de forma ms expresiva
empleando un fichero yaml para usarlo con Alice.
https://github.com/nelmio/alice
Matthias Noback
Matthias (@matthiasnoback) es un programador de PHP profesional y experimentado que ha
dado unas cuantas conferencias en la comunidad de PHP. Es el autor del libro A year with
Symfony, (disponible tambin en castellano) y del blog PHP & Symfony About PHP and Symfony2
development.
Sigo su blog habitualmente con bastante inters puesto que suele tener posts relacionados con las
buenas prcticas en el uso de Symfony. Matthias ha comenzado a escribir una serie de posts sobre
su experiencia de testing con PHP y pens que tena que trasladar esto a este libro puesto que
estamos hablando de lo mismo. Me puse en contacto con Matthias y amablemente accedi a que
le entrevistase y traducir sus artculos sobre testing para este libro.
Mi interesante su visin del testing, con consejos y trucos concretos.
Matthias Noback
69
Matthias Noback
70
Matthias Noback
71
Matthias Noback
72
Tal vez, en lugar de vete de mi jardn debo comenzar diciendo ven y sintate en el
porche conmigo.
Por que estoy contando todo esto? Bien, por diferentes razones. Lo primero, TDD no es lo mismo
que testing (quizs hayas advertido que yo ya he hecho un gran salto conceptual entre estas dos
palabras en los prrafos anteriores). Esto significa que TDD puede estar muerto, pero al mismo
tiempo el testing puede estar muy vivo (lo que creo que es el caso). De todas formas, TDD es una
tcnica que necesitas dominar como desarrollador y deberas aprenderlo. Pero no puedes aprender
sobre testing de otros desarrolladores que te hacen sentir avergonzado por algo que todava no has
aprendido (o no lo has hecho bien). La solucin? Todos los testers experimentados en el mundo
deberan volverse mentores de alguien que sea un novato en el testing. As que esta es mi llamada
de atencin:
Si conoces a alguien que no sabe cmo escribir tests, o no lo hace, ensale.
Y si t eres ese alguien:
Encuentra alguien que te ensee cmo escribir mejores tests que los que haces y
pregntale para qu te ensee cmo lo hace.
Hay algn libro, post o artcuo que te haya abierto los ojos?
Bien, he aprendido un montn de Growing Object-Oriented Software, Guided by Tests. Bridging
the Communication Gap: Specification by Example and Agile Acceptance Testing tambin es
una lectura interesante. Algunos artculos de Robert Martin contienen un montn de reflexiones
http://www.amazon.com/Growing-Object-Oriented-Software-Guided-Tests/dp/0321503627/ref=pd_sim_b_6?ie=UTF8&refRID=
0WBMXN6M4DA6H3FP0XMR
http://www.amazon.com/Bridging-Communication-Gap-Specification-Acceptance/dp/0955683610/ref=sr_1_fkmr0_1?s=books&ie=
UTF8&qid=1405161330&sr=1-1-fkmr0&keywords=Gojko+Ad%C5%BEi%C4%87
http://blog.8thlight.com/uncle-bob/archive.html
Matthias Noback
73
Quien es Pawe?
Soy un emprendedor y desarrollador de software que vive en Polonia con mi adorable pareja. He
creado el proyecto Sylius y he empezado Lakion, que es una compaa provee todo tipo de servicios
comerciales.
75
Qu es Sylius?
Sylius es un proyecto completamente open source (con licencia MIT) de plataforma de comercio
electrnico para PHP y que intenta ser la solucin definitiva para desarrolladores trabajando en
plataformas de venta online.
76
encontrarme con gente como Kostantin Kudryshov, que me inspir para sumergirme en el BDD y
volverme un apasionado de la calidad. Lo hice y ahora no puedo volver atrs. En mi trabajo siempre
uso BeHat y PHPSpec para prcticamente todo y con grandes resultados. Todo comienza escribiendo
una funcionalidad y describirla usando Gherkin, viendo el color rojo y entonces escribiendo las
especificaciones en cdigo para soportar esa funcionalidad. Tan pronto como veo el color verde,
podemos refactorizar el cdigo para hacerlo ms elegante y continuar. Algunas veces me encuentro
a mi mismo implementando una funcionalidad al completo sin tan siquiera abrir un navegador.
Los escenarios de BeHat, requieren inicializar la base de datos, pero una vez que lo has hecho los
puedes ejecutar simplemente con:
1
$ bin/behat --suite=account
77
78
que todo el mundo se encuentre en la misma pgina sobre los requerimientos de negocio. Slo los
tontos pensarn que escribir cdigo es la parte ms difcil de nuestro trabajo. Lo es comprender
y enfocarnos en las funcionalidades que debemos de entregar, que ofrezcan el mayor valor para
nuestro cliente. Desde un punto de vista tcnico, BeHat es una herramienta que permite trabajar
realmente bien con esto. Si t tienes un montn de escenarios y pasos complejos, el rendimiento
puede ser un problema, pero esto slo afecta a conjuntos muy grandes de funcionalidades. Con la
versin tres de BeHat, hay una mejora significativa y cuando la ejecucin en paralelo llegue a estar
estable entonces el problema debera estar bastante bien solucionado.
Nivel humano
Este proyecto lo desarrollo con otras dos personas, una es un programador que ya tena experiencia
con Codeception y con otra, que aunque no tiene un perfil tcnico, es capaz de leer pequeos
fragmentos de cdigo.
BDD trata sobre comunicacin y de expresar en un lenguaje comn de alto nivel y claro las
especificaciones que debe cumplir el software. Estas especificaciones deben poder comprobarse de
forma automtica. Como el software no es para ningn cliente externo, y entre los 3 somos capaces
de leer bien los test de aceptacin, estamos cumpliendo el objetivo de la comunicacin. El lenguaje,
an siendo PHP, resulta bastante claro de leer y la salida por pantalla es muy legible.
1
2
3
4
5
6
<?php
$I = new AcceptanceTester($scenario);
$I->am('Account Holder');
$I->wantTo('withdraw cash from an ATM');
$I->lookForwardTo('get money when the bank is closed');
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
80
Feature: ls
In order to see the directory structure
As a UNIX user
I need to be able to list the current directory's contents
Scenario:
Given I am in a directory "test"
And I have a file named "foo"
And I have a file named "bar"
When I run "ls"
Then I should get:
"""
bar
foo
"""
$this->shouldHaveType('Movie');
$this->assertInstanceOf('Movie');
81
Tampoco es una gran diferencia, pero resulta ms cmodo con PhpSpec. Adems, cuando se genera
algn error de PHP, se gestiona mejor con PhpSpec.
En la versin 4.5 de PhpUnit se incorpora Prophecy como framework de mocking de objetos, que es
el mismo que usa PhpSpec, as que no hay diferencia a la hora de crear mocks entre Codeception y
PhpSpec
Documentacin
PhpSpec y Behat son bastante nuevos y a la hora de buscar documentacin se nota. Hay mucha gente
que los est usando pero falta una documentacin ms extensa y avanzada. Hay miles de ejemplos
sencillos pero cuando avanzas te sientes un poco perdido.
La documentacin de Codeception est bastante bien, ms o menos al nivel de la de PhpSpec y Behat,
pero como los tests unitarios los haces con PhpUnit y ste es todo un veterano, no tienes problema en
encontrar lo que necesites. Con la parte de los tests de aceptacin, al estar escritos en PHP normal,
resulta ms sencillo poder buscarte la vida.
Extensibilidad
Se me ha dado el caso de querer hacer unos tests muy parecidos para diferentes clases con PhpSpec.
Como los tests tambin es algo que hay que mantener, he querido usar traits (si, no pasa nada por
usar traits, no vas al infierno de los programadores). Habl con el autor de PhpSpec al respecto y
aunque reconoci que para mi caso podan ser necesarios, me confirm que no poda usarlos por
temas de la carga de clases, as que acab haciendo copy/paste de unas cuantas lneas de cdigo.
Con Codeceptin no sucede este problema. Desde el principio te explican en la documentacin dnde
debes incluir tus mtodos y clases para poder reutilizar cdigo.
La experiencia con Behat ha sido parececida en este sentido.
82
Behat y PhpSpec me han resultado mucho ms cmodos que Codeception para escribir los tests a
priori. Behat porque el lenguaje Gherkin es casi como escribir en castellano lo que quieres hacer y
PhpSpec porque te ayuda con la creacin de las clases y mtodos que debes implementar crendolos
por ti a partir los tests.
En este sentido Codeceptin es ms incmodo si vas a hacer lo que sea driven development, pero me
ha resultado el ms cmodo al escribir los tests a posteriori.
Tests funcionales
Este es el punto clave. En un test de aceptacin se interacta completamente a travs de un
navegador. Por lo tanto no hay posibilidad de manejar variables tipo objeto ni debes tener aserciones
sobre aspectos tcnicos como un objeto de base de datos. Por ejemplo, en un test funcional usas un
falso navegador y compruebas el resultado de tu accin mirando en la base de datos.
En este proyecto no me encargo del frontend y adems es algo que va cambiando. Me resulta difcil
escribir test que busquen un id de html, o cadenas de texto en el navegador. No me gusta estar
cambiando los tests cada vez que alguien decide que un texto pasa de ser un listado de tags a un
listado de categoras o porque la persona que gestiona el frontend ha decidido cambiar el id. A fin
de cuentas mi funcionalidad no ha cambiado en absoluto.
Codeception tiene un mdulo de Symfony que nos permite acceder al contenedor de dependencias
y al kernel de forma sencilla resultando fcil hacer comprobaciones de BD, emails etc.
Behat tiene una extensin para Symfony que tambin nos permite acceder al kernel de una
aplicacin Symfony. Lo que sucede es que su utilidad no la veo tan clara puesto que la interaccin
de los tests de aceptacin debe ser a traves del navegador. Viene bien, por ejemplo, para saber si se
ha enviado un mail o simular un login de usuario modificando la sesin directamente.
83
Si hago otro proyecto en el que fuese a hacer BDD/TDD estricto desde el principio, seguramente
volvera a usar PhpSpec y Behat. Si tuviera un proyecto en el que el cliente fuese a leerse los tests
de aceptacin y a colaborar en su creacin, usara Behat.
No s cmo encajan estas soluciones de testing con toda la revolucin de frontend que tenemos hoy
en da. Tenemos verdaderas aplicaciones en frontend hechas con Javascript y que a su vez usan sus
propios frameworks de testing.