Tipos de test de software TDD

Tipos de test de software

El siguiente contenido es un estracto del libro "Diseño Ágil con TDD" de Carlos Blé Jurado y colaboradores.

Existen diferentes test según el modo de clasificarlos.

Según el propietario

  • Test propiedad del cliente (Product Owner)
    • Test de aceptación: son escritos con ayuda de un analista durante la fase de planificación.
    • Test funcional: son escritos una vez que el producto ya es usable.
  • Test propiedad de los programadores
    • Test unitario.
    • Test de integración.
    • Test de sistema.
Tipos de test

Test de aceptación

Es un test que permite comprobar que el software cumple con un requisito de negocio. ¿Los tests de aceptación no usan la interfaz de usuario del programa? Podría ser que sí, pero en la mayoría de los casos la respuesta debe ser no. Los tests de carga y de rendimiento son de aceptación cuando el cliente los considera requisitos de negocio. Si el cliente no los requiere, serán tests de desarrollo.

Test funcionales

Todos los tests son en realidad funcionales, puesto que todos ejecutan alguna función del SUT (Subject Under Test, o código que se está probando), aunque en el nivel más elemental sea un método de una clase. No obstante, cuando se habla del aspecto funcional, se distingue entre test funcional y test no funcional. Un test funcional es un subconjunto de los tests de aceptación. Es decir, comprueban alguna funcionalidad con valor de negocio. Los tests de aceptación tienen un ámbito mayor porque hay requerimientos de negocio que hablan de tiempos de respuesta, capacidad de carga de la aplicación, etc; cuestiones que van más allá de la funcionalidad. Un test funcional es un test de aceptación pero, uno de aceptación, no tiene por qué ser funcional.

Test de sistema

Es el mayor de los tests de integración, ya que integra varias partes del sistema. Se trata de un test que puede ir, incluso, de extremo a extremo de la aplicación o del sistema. Se habla de sistema en referencia a la integración de todos los elementos que componen la aplicación. Así pues, un test del sistema se ejecuta tal cual lo haría el usuario humano, usando los mismos puntos de entrada (aquí sí es la interfaz gráfica) y llegando a modificar la base de datos o lo que haya en el otro extremo.

Los tests de sistema son muy frágiles en el sentido de que cualquier cambio en cualquiera de las partes que componen el sistema, puede romperlos. No es recomendable escribir un gran número de ellos por su fragilidad. Si la cobertura de otros tipos de tests de granularidad más fina, como por ejemplo los unitarios, es amplia, la probabilidad de que los errores sólo se detecten con tests de sistema es muy baja. O sea, que si hemos ido haciendo TDD, no es productivo escribir tests de sistema para todas las posibles formas de uso del sistema, ya que esta redundancia se traduce en un aumento del costo de mantenimiento de los tests.

La diferencia entre los tests de sistema y los funcionales

Los tests de sistema son diferentes de los tests funcionales y de aceptación. Sin embargo, a veces se pueden confundir. Un test funcional trata de probar que se cumple un cierto requisito de negocio que el cliente considera de valor. Por ejemplo, si estuviésemos creando un cliente para un servicio de correo electrónico, un requisito de negocio podría ser:

En el caso de intentar iniciar sesión con una cuanta de correo errónea, la aplicación devuelve un mensaje de respuesta.

El requisito de negocio no habla de la GUI en ningún momento. Por tanto, el test funcional no entraría a ejecutar el sistema desde el extremo de entrada que usa el usuario (la GUI). Su objetivo es únicamente comprobar que el requisito se cumple.

Si la mayoría de los criterios de aceptación se validan mediante tests funcionales, tan sólo nos harán falta unos pocos tests de sistema para comprobar que la GUI está bien conectada a la lógica de negocio. Esto hará que nuestros tests sean menos frágiles y estaremos alcanzando el mismo nivel de cobertura de posibles errores.

En la documentación de algunos frameworks, llaman test unitarios a tests que en verdad son de integración y, llaman tests funcionales, a tests que son de sistema. Llamar test funcional a un test de sistema no es un problema siempre que adoptemos esa convención en todo el equipo y todo el mundo sepa para qué es cada test.

En casos puntuales, un requisito de negocio podría involucrar la GUI, simpre que lo el requisito lo indique específicamente.

Test unitario

Son los tests más importantes para el practicante TDD. Cada test unitario o test unidad (unit test en inglés) es un paso que andamos en el camino de la implementación del software. Todo test unitario debe ser:

  • Atómico: el test prueba la mínima cantidad de funcionalidad posible. Esto es, probará un solo comportamiento de un método de una clase. El mismo método puede presentar distintas respuestas ante distintas entradas o distinto contexto. El test unitario se ocupará exclusivamente de uno de esos comportamientos, es decir, de un único camino de ejecución. A veces, la llamada al método provoca que internamente se invoque a otros métodos; cuando esto ocurre, decimos que el test tiene menor granularidad, o que es menos fino. Lo ideal es que los tests unitarios ataquen a métodos lo más planos posibles, es decir, que prueben lo que es indivisible. La razón es que un test atómico nos evita tener que usar el depurador para encontrar un defecto en el SUT, puesto que su causa será muy evidente.
  • Independiente: significa que un test no puede depender de otros para producir un resultado satisfactorio. No puede ser parte de una secuencia de tests que se deba ejecutar en un determinado orden. Debe funcionar siempre igual independientemente de que se ejecuten otros tests o no.
  • Inocuo: no altera el estado del sistema. Al ejecutarlo una vez, produce exactamente el mismo resultado que al ejecutarlo veinte veces. No altera la base de datos, ni envía emails ni crea ficheros, ni los borra. Es como si no se hubiera ejecutado.
  • Rápido: un test unitario debe ejecutarse en una fracción de segundo, porque grandes pilas de tests se ejecutarán varias veces al día, conforme vayamos haciendo cambios (y refactorizando) nuestro código.

Las características de un test unitario también quedan descritas por el acrónimo F.I.R.S.T (Fast, Independent, Repeatable, Small y Transparent).

Test de integración

Por último, los tests de integración son la pieza del puzzle que nos faltaba para cubrir el hueco entre los tests unitarios y los de sistema. Los tests de integración se pueden ver como tests de sistema pequeños. Tienen un aspecto parecido a los tests unitarios, sólo que estos pueden romper las reglas. Como su nombre indica, integración significa que ayuda a unir distintas partes del sistema. Un test de integración puede escribir y leer de base de datos para comprobar que, efectivamente, la lógica de negocio entiende datos reales. Es el complemento a los tests unitarios, donde habíamos "falseado" el acceso a datos para limitarnos a trabajar con la lógica de manera aislada. Un test de integración podría ser aquel que ejecuta la capa de negocio y después consulta la base de datos para afirmar que todo el proceso, desde negocio hacia abajo, fue bien. Son, por tanto, de granularidad más gruesa y más frágiles que los tests unitarios, con lo que el número de tests de integración tiende a ser menor que el número de tests unitarios. Una vez que se ha probado que dos módulos, objetos o capas se integran bien, no es necesario repetir el test para otra variante de la lógica de negocio; para eso habrán varios tests unitarios. Aunque los tests de integración pueden saltarse las reglas, por motivos de productividad es conveniente que traten de ser inocuos y rápidos. Si tiene que acceder a base de datos, es conveniente que luego la deje como estaba.

Los tests unitarios deben pertenecer a suites de tests diferentes para poder ejecutarlos por separado.

Otros puntos de vista

Robert Martin (Uncle Bob) habla en este artículo sobre la razón del fracaso de muchos equipos que utilizan Scrum y TDD.

En este artículo, podemos conocer sobre los tests vistos desde el punto de vista de un programador.

Los tipos de prueba planteados como las capas de una cebolla

TDD

TDD son las siglas de Test Driven Development (Desarrollo Guiado por Pruebas). TDD requiere tres pasos para escribir código:

  1. Escribir primero la especificación del requisito (el test).
  2. Implementar el código para cubrir la prueba.
  3. Refactorizar para mejorar el código y eliminar duplicidades

Estos pasos implican que el código escrito es el mínimo necesario para que las pruebas se cumplan (siguiendo el principio KISS, es decir "Keep It Simple, Stupid"). TDD convive con la refactorización, ya que conforme se va escribiendo código, se van produciendo defectos en el código, como duplicidades, por ejemplo, que requieren su reescritura. Más adelante recrearemos un ejemplo con esta filosofía para comprenderlo mejor.

Las pruebas deben escribirse de forma que hayan que cambiarlas lo menos posible. Es decir, hay que pensar bien cómo querríamos que fuese (de forma abstracta) nuestro software, y después implementarlo.

Instalar PHPUnit

PHPUnit es un framework utilizado para escribir tests en PHP. Podemos instalarlo de dos maneras diferentes:

Instar PHPUnit mediante Composer

Podemos utilizar Composer para instalar la última versión de PHPUnit. Es importante que PHPUnit sea un requisito de desarrollo, para que PHPUnit no se despliegue en el sistema en producción. El archivo composer.json correspondiente podría ser algo como lo siguiente:

{ "require-dev":{ "phpunit/phpunit":">=5.1.3-stable" } }

Instalando mediante Composer, obtendremos PHPUnit como una dependencia del proyecto.

Para comprobar que todo ha ido bien, debemos ejecutar el siguiente comando dentro de la carpeta del proyecto:

vendor/bin/phpunit --version

PHPUnit en un único archivo

PHPUnit también se puede instalar como un único archivo, que incluye todas sus dependencias. Para instalar dicho archivo, debemos seguir las instrucciones que se dan en https://packagist.org/packages/phpunit/phpunit

Para comprobar que todo ha ido bien, basta con ejecutar el siguiente comando:

phpunit --version

Un primer ejemplo

Vamos a trabajar con un primer ejemplo. La estructura de nuestro proyecto es la siguiente:

aprendophpunit/index.php aprendophpunit/modelo/Salario.php composer.json vendor/

Supongamos que el código que queremos probar, en la clase Salario (Salario.php), es el siguiente:

namespace aprendophpunit\modelo; class Salario { public function incrementoSalario($salarioActual){ return $salarioActual+$salarioActual*2/100; } public function retencionSalario($salarioBruto){ return $salarioBruto*22/100; } }

Para probar que todo es correcto, vamos en primer lugar a probar la clase en index.php. El contenido de index.php podría ser el siguiente:

<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title></title> </head> <body> <?php require "vendor/autoload.php"; use aprendophpunit\modelo\Salario; $salario = new Salario(); echo "El aumento de salario es ".$salario->incrementoSalario(2000); ?> </body> </html>

Veremos que esto falla, porque aún no hemos incluido la clase Salario en el autoload de Composer. Podemos modificar composer.json para que quede como en el siguiente ejemplo:

{ "require-dev":{ "phpunit/phpunit":">=5.1.3-stable" }, "autoload":{ "psr-4":{ "aprendophpunit\\modelo\\":"modelo/" } } }

Después de esto, actualizamos Composer como la opción dump-autoload. Después de esto debería funcionar nuestro pequeño proyecto.

Ubicación y nombre de los tests

Es importante la estructura de directorios que damos a nuestros tests. Lo ideal es que siguan una estructura similar a la que sigue el código probado, de esta forma podemos identificar claramente qué componente prueba un cierto test. De modo que si tenemos los siguientes archivos:

aprendophpunit/modelo/Salario.php aprendophpunit/modelo/Controlador/Foo.php

Los archivos de test, podrían estructurarse del siguiente modo:

aprendophpunit/Test/modelo/SalarioTest.php aprendophpunit/Test/modelo/Controlador/FooTest.php

Un primer test unitario

Vamos a crear un test para salario. Su contenido será el siguiente:

<?php /* * To change this license header, choose License Headers in Project Properties. * To change this template file, choose Tools | Templates * and open the template in the editor. */ /** * Description of SalarioTest * * @author mauri */ /* namespace aprendophpunit\Test\modelo; require 'vendor/autoload.php'; // Esto es necesario para poder acceder a Salario. use aprendophpunit\modelo\Salario; class SalarioTest extends \PHPUnit_Framework_TestCase{ public function testSalarioDevuelveIncremento20porciento(){ $incremento20 = 2000*2/100; $salario = new Salario(); $salarioObtenido = $salario->incrementoSalario(2000); $salarioEsperado = 2000 + $incremento20; $this->assertEquals($salarioEsperado,$salarioObtenido,"Los salarios son iguales."); } public function testRetencionSalario22porciento(){ $retencionEsperada = 2000*22/100; $salario = new Salario(); $retencionObtenida = $salario->retencionSalario(2000); $this->assertEquals($retencionEsperada,$retencionObtenida); } }

En el ejemplo anterior podemos observar varias cuestiones:

El nombre del caso de prueba

Un caso de prueba es una clase que agrupa un conjunto de pruebas unitarias. El nombre del caso de prueba (o test case) es igual que la clase que estamos probando, con la palabra Test al final.

PHPUnit_Framework_TestCase

Un case de preuba hereda de la clase PHPUnit_Framework_TestCase

Nombre de los tests unitarios

Los nombres de los tests unitarios son largos y descriptivos. Empiezan por la palabra "test", seguidos en notación camelCase de la descripción del test.

Ejecutar una suite de tests

Al conjunto de casos de prueba, se le llama siute de tests. Para poder ejecutar los tests, es preciso crear un archivo llamado phpunit.xml en la raíz del proyecto. El contenido será similar al siguiente:

<?xml version="1.0" encoding="UTF-8"?> <phpunit colors="true"> <testsuites> <testsuite name="Application Test Suite"> <directory>./Test/</directory> </testsuite> </testsuites> </phpunit>

Lo que hacemos en este archivo es:

  • Indicar que se utilicen colores para indicar si el test ha sido positivo o negativo (verde/rojo).
  • Indicar la ruta donde se encuentran los tests

Para ejecutar los tests, abrimos una terminal, y nos vamos hasta la raíz del proyecto. Allí ejecutamos el comando siguiente:

vendor/bin/phpunit

El resultado debería ser algo como lo siguiente:

Resultado de ejecutar los tests.

PHPUnit y su integración con NetBeans

PHPUnit se integra bien con NetBeans, aunque no se lleva bien con las configuraciones hechas a mano. Es decir, si vamos a usar NetBeans con PHPUnit, debemos hacerlo desde el primer momento. Para utilizar PHPUnit con NetBeans, podemos usar el proyecto utilizado hasta el momento, pero debemos borrar la carpeta "Test" así como el archivo "phpunit.xml".

Indicando a NetBeans donde está phpunit

Con NetBeans debemos utilizar una instalación global de PHPUnit en /usr/local/bin/ (ver sección "Installation" en https://packagist.org/packages/phpunit/phpunit

Además, necesitamos PHPUnit Skeleton Generator, que creará los directorios y clases de casos de prueba automáticamente (ver apartado "Installation").

A continuación, configuramos NetBeans para que utilice sendos scripts, en tools\options\PHP\Frameworks & Tools\PHPUnit, tal y como se ve en la siguiente imagen.

Configurando PHPUnit en NetBeans.

Indicando al proyecto donde está su directorio de tests

NetBeans debe saber donde está el directorio donde se almacenarán los tests. Para ello, vamos a crear un directorio donde almacenar los tests. Por ejemplo (para variar) un directorio llamado "tests". Podemos indicar esto en Propiedades del proyecto\Testing\Add folder

Ruta al directorio de tests.

Después de esto, nos aparecerá una nueva sección en el proyecto llamada Test Files

Creando un nuevo test

Para crear un nuevo test, hacemos clic derecho sobre la clase que queramos probar. Entonces elegimos la opción tools\create/update tests. Supongamos que estamos sobre la clase "Salario.php". Después de esto (se nos pregunta por "Location" y "Framework", que dejamos a los valores indicados, es decir "Test Files" y "PhpUnit") se creará una nueva clase con el siguiente código:

<?php namespace aprendophpunit\modelo; require '../vendor/autoload.php'; /** * Generated by PHPUnit_SkeletonGenerator on 2015-12-29 at 19:37:22. */ class SalarioTest extends \PHPUnit_Framework_TestCase { /** * @var Salario */ protected $object; /** * Sets up the fixture, for example, opens a network connection. * This method is called before a test is executed. */ protected function setUp() { $this->object = new Salario; } /** * Tears down the fixture, for example, closes a network connection. * This method is called after a test is executed. */ protected function tearDown() { } /** * @covers aprendophpunit\modelo\Salario::incrementoSalario * @todo Implement testIncrementoSalario(). */ public function testIncrementoSalario() { // Remove the following lines when you implement this test. $this->markTestIncomplete( 'This test has not been implemented yet.' ); } /** * @covers aprendophpunit\modelo\Salario::retencionSalario * @todo Implement testRetencionSalario(). */ public function testRetencionSalario() { // Remove the following lines when you implement this test. $this->markTestIncomplete( 'This test has not been implemented yet.' ); } }

Si te fijas, verás que hay una línea resaltada. Es necesario añadir esta línea para que no de error al cargar la clase Salario. De otro modo no la encontraría.

Observa que en los tests aparecen dos funciones nuevas:

  • setUp(): se ejecuta antes de empiecen los tests. Nos vienen bien para inicializar objetos que reutilizaremos una y otra vez a lo largo de los tests. Esto ahorra tiempo para crear y destruir objetos iguales una y otra vez (ya que los tests deben ser rápidos).
  • tearDown(): acciones a llevar a cabo al final de los tests. Si nuestros tests han hecho cambios que debemos deshacer, por ejemplo, este es el sitio donde hacerlo.

Tansolo nos queda escribir las pruebas unitarias. El código de "SalarioTest" quedará como el siguiente:

<?php namespace aprendophpunit\modelo; require '../vendor/autoload.php'; /** * Generated by PHPUnit_SkeletonGenerator on 2015-12-29 at 19:37:22. */ class SalarioTest extends \PHPUnit_Framework_TestCase { /** * @var Salario */ protected $object; /** * Sets up the fixture, for example, opens a network connection. * This method is called before a test is executed. */ protected function setUp() { $this->object = new Salario; } /** * Tears down the fixture, for example, closes a network connection. * This method is called after a test is executed. */ protected function tearDown() { } /** * @covers aprendophpunit\modelo\Salario::incrementoSalario * @todo Implement testIncrementoSalario(). */ public function testIncrementoSalario() { $salarioEsperado = 2000 + 2000*2/100; $salarioObtenido = $this->object->incrementoSalario(2000); return $this->assertEquals($salarioEsperado,$salarioObtenido); } /** * @covers aprendophpunit\modelo\Salario::retencionSalario * @todo Implement testRetencionSalario(). */ public function testRetencionSalario() { $retencionEsperada = 2000*22/100; $retencionObtenida = $this->object->retencionSalario(2000); return $this->assertEquals($retencionEsperada,$retencionObtenida); } }

Ya podemos ejecutar nuestro test. Para ello, hacemos clic derecho sobre el proyecto y elegimos la opción Test.

NetBenas tras ejecutar el Test "SalarioTest".

Asertos

Ahora ya nos podemos centrar en la lógica de los tests. Un aserto es un predicado (que puede ser verdadero o falso) que se ubica en un programa para indicar que el programador piensa que ese predicado es siempre verdad en ese lugar. Hay muchos asertos, que están listados aquí. Los asertos siempre tienen la forma $this->assert*. Los más habituales serán assertArrayHasKey, assertEquals, assertFalse, assertSame y assertTrue

Proveedores de datos

Puede que quiera usar varias veces un mismo test con diferentes datos de entrada. Podría reescribir varias veces el mismo test, pero eso va en contra de DRY (Don't Repeat Yourself). Para esto están los proveedores de datos. En el siguiente ejemplo se usa varias veces el mismo test:

<?php namespace aprendophpunit\modelo; require '../vendor/autoload.php'; /** * Generated by PHPUnit_SkeletonGenerator on 2015-12-29 at 19:37:22. */ class SalarioTest extends \PHPUnit_Framework_TestCase { /** * @var Salario */ protected $object; /** * Sets up the fixture, for example, opens a network connection. * This method is called before a test is executed. */ protected function setUp() { $this->object = new Salario; } /** * Tears down the fixture, for example, closes a network connection. * This method is called after a test is executed. */ protected function tearDown() { } /** * @covers aprendophpunit\modelo\Salario::incrementoSalario * @todo Implement testIncrementoSalario(). * @dataProvider providerIncrementoSalario */ public function testIncrementoSalario($salarioBase, $salarioIncrementado) { $salarioEsperado = $salarioIncrementado; $salarioObtenido = $this->object->incrementoSalario($salarioBase); return $this->assertEquals($salarioEsperado,$salarioObtenido); } /** * @covers aprendophpunit\modelo\Salario::retencionSalario * @todo Implement testRetencionSalario(). */ public function testRetencionSalario() { $retencionEsperada = 2000*22/100; $retencionObtenida = $this->object->retencionSalario(2000); return $this->assertEquals($retencionEsperada,$retencionObtenida); } public function providerIncrementoSalario(){ return array( 'salario 1000' => array(1000,1020), 'salario 2000' => array(2000,2040), 'salario 3000' => array(3000,3060) ); } }

En blanco aparecen los elementos que son necesarios para utilizar un proveedor. Si ejecutamos este test, obtendremos 4 tests.

Tests realizados mediante proveedores de datos.

Aplicando TDD para escribir un proyecto.

Vamos a escribir una sencilla calculadora utilizando TDD. Vamos a basarnos en el capítulo 8 del libro Diseño Ágil con TDD de Carlos Blé.

En este capítulo, se pude observar que las especificaciones iniciales son bastante breves, y que a medida que se va avanzando en el proyecto, van surgiendo preguntas que dan lugar a nuevos requisitos, y nuevos tests unitarios.

Los requisitos iniciales

Los requisitos de partida son breves:

  • "2 + 2", devuelve 4
  • "5 + 4 * 2 / 2", devuelve 9
  • "3 / 2", produce el mensaje ERROR
  • "* * 4 - 2": produce el mensaje ERROR
  • "* 4 5 - 2": produce el mensaje ERROR
  • "* 4 5 - 2 -": produce el mensaje ERROR
  • "*45-2-": produce el mensaje ERROR

Los requisitos tras un poco de trabajo

Conforme van surgiendo dudas (dado que los requisitos iniciales son muy breves), se va consultando al Product Owner, de forma que se van añadiendo requisitos:

  • "2 + 2", devuelve 4
  • "5 + 4 * 2 / 2", devuelve 9
  • "3 / 2", produce ERROR
  • "* * 4 - 2": produce ERROR
  • "* 4 5 - 2": produce ERROR
  • "*45-2-": produce ERROR
  • "2 + -2"devuelve 0
  • Límite Superior =100
  • Límite Superior =500
  • Límite Inferior = -1000
  • Límite Inferior = -10

Los requisitos y los tests

Los requisitos sirven de guía al programador para crear sus tests. Así, para el primer requisito, podríamos crear los siguientes tests:

  • Aceptación: "2 + 2", devuelve 4
    • Test: Sumar 2 al número 2, devuelve 4
    • Test: Sumar 5 al número 2, devuelve 7
    • Test: Restar 3 al número 5, devuelve 2
    • Test: Restar 5 al número 3, devuelve -2

El requisito "5 + 4 * 2 / 2", devuelve 9 podría dar lugar a los siguientes tests:

  • Aceptación: "5 + 4 * 2 / 2", devuelve 9
    • Test: "5 + 4 * 2 / 2", devuelve 9
    • Test: "6 + 2 * 4 / 2", devuelve 10
    • Test: "6 * 5 - 8 / 2 + 1", devuelve 27

Por otra parte, el requisito "2 + -2"devuelve 0 podría dar lugar a los siguientes tests:

  • Aceptación: "2 + -2"devuelve 0
    • Test: "2 + -2", devuelve 0
    • Test: "5 + -5", deuvelve 0
    • Test: "5 + - 3", devuelve 2
    • Test: "-330 + 30", devuelve -300

No es necesario ser cuadriculado con el orden de los tests, ya que puede interesarnos hacer primero ciertos tests, por alguna razón:

  • Porque son más sencillos
  • Porque me interesan más desde el punto de vista arquitectónico
  • Etc.

En el libro citado (Diseño Ágil con TDD) se indica lo siguiente:

En TDD resolvemos el problema como si de un árbol se tratase. La raíz es el test de aceptación y los nodos hijos son tests de desarrollo, que unas veces serán unitarios y otras veces quizás de integración. Un árbol puede recorrerse de dos maneras; en profundidad y en amplitud. La decisión la tomamos en función de nuestra experiencia, buscando lo que creemos que nos va a dar más beneficio, bien en tiempo o bien en prestaciones. No hay ninguna regla que diga que primero se hace a lo ancho y luego a lo largo".

En nuestro caso, empezaremos por los tests que solo requieran dos argumentos, por simplicidad.

Creando nuestro software. Preparar el proyecto.

Empecemos por el siguiente requisito:

  • Aceptación: "2 + 2", devuelve 4
    • Test: Sumar 2 al número 2, devuelve 4
    • Test: Sumar 5 al número 2, devuelve 7
    • Test: Restar 3 al número 5, devuelve 2
    • Test: Restar 5 al número 3, devuelve -2

Debemos crear nuestro proyecto. Dicho proyecto puede tener un controlador frontal "index.php", un directorio donde colocar nuestras primeras clases. Además, podemos utilizar Composer para simplificar la carga de archivos:

calculadora/index.php <!DOCTYPE html> <html> <head> <title>Calculadora</title> </head> <body> <?php require "vendor/autoload.php"; use calculadora\operaciones\OperadorBinario; $operadorBinario = new OperadorBinario(); ?> </body> </html>
calculadora/composer.json { "require-dev":{ "phpunit/phpunit":"5.1.3" }, "autoload":{ "psr-4":{ "calculadora\\operaciones\\":"operaciones/" } } }

Dado que las operaciones que se realizan en la prueba de aceptación son de tipo binarias (dos operandos), vamos a crear una clase llamada "OperadorBinario", que no hace nada de momento.

calculadora/operaciones/OperadorBinario.php <?php /** * Description of OperadorBinario * * @author mauri */ namespace calculadora\operaciones; class OperadorBinario { public function suma(){ } }

Y después de configurar los PHPUnit como software de testing para el proyecto, creamos un caso de prueba como el siguiente para la clase OperadorBinario:

calculadora/tests/operaciones/OperadorBinarioTest.php <?php namespace calculadora\operaciones; require '../vendor/autoload.php'; /** * Generated by PHPUnit_SkeletonGenerator on 2015-12-30 at 16:08:19. */ class OperadorBinarioTest extends \PHPUnit_Framework_TestCase { /** * @var OperadorBinario */ protected $object; /** * Sets up the fixture, for example, opens a network connection. * This method is called before a test is executed. */ protected function setUp() { $this->object = new OperadorBinario; } /** * Tears down the fixture, for example, closes a network connection. * This method is called after a test is executed. */ protected function tearDown() { } }

La primera prueba

  • Aceptación: "2 + 2", devuelve 4
    • Test: Sumar 2 al número 2, devuelve 4
    • Test: Sumar 5 al número 2, devuelve 7
    • Test: Restar 3 al número 5, devuelve 2
    • Test: Restar 5 al número 3, devuelve -2

Añadimos a nuestro test la siguiente prueba.

calculadora/tests/operadores/OperadorBinarioTest ... public function testSumaDosNumerosPositivos(){ $numeroEsperado = 4; $numeroObtenido = $this->operadorBinario->suma(2,2); $this->assertEquals($numeroEsperado,$numeroObtenido); } ...

Si probamos nuestro proyecto debería darnos en rojo.

El proceso TDD exige escribir primero los tests, y pasar del "rojo" al "verde".

Ahora escribimos el código mínimo para pasar el test:

calculadora/operadores/OperadorBinario.php ... public function suma(){ return 4; } ...

El test pasa:

El test pasa con una cantidad mínima de código

La segunda prueba

  • Aceptación: "2 + 2", devuelve 4
    • Test: Sumar 2 al número 2, devuelve 4
    • Test: Sumar 5 al número 2, devuelve 7
    • Test: Restar 3 al número 5, devuelve 2
    • Test: Restar 5 al número 3, devuelve -2

Una de las condiciones que pone TDD es que las pruebas deben ser código fuente de calidad. Es decir, que también se debe refactorizar cuando haga falta. Para hacer esta segunda prueba, deberíamos volver a escribir una nueva prueba, o bien repetir código. Por ello, es el momento de utilizar un "proveedor de datos" en lugar de escribir otra prueba casi igual que la anterior.

calculadora/tests/operadores/OperadorBinarioTest.php ... /** * @dataProvider providerSumaDosNumerosPositivos */ public function testSumaDosNumerosPositivos($num1,$num2,$numeroEsperado){ $numeroObtenido = $this->operadorBinario->suma($num1,$num2); $this->assertEquals($numeroEsperado,$numeroObtenido); } public function providerSumaDosNumerosPositivos(){ return array( '2 + 2 = 4' => array(2,2,4), '5 + 2 = 7' => array(5,2,7) ); } ...
Es preciso modificar el código fuente para que pase el segundo test.

Modificamos el código fuente para que pase el segundo test:

calculadora/operadores/OperadorBinario.php ... public function suma($num1, $num2){ return $num1 + $num2; } ...

Y ahora los tests pasan.

Todos los tests pasan hasta ahora.

La tercera prueba...

  • Aceptación: "2 + 2", devuelve 4
    • Test: Sumar 2 al número 2, devuelve 4
    • Test: Sumar 5 al número 2, devuelve 7
    • Test: Restar 3 al número 5, devuelve 2
    • Test: Restar 5 al número 3, devuelve -2

Esta prueba requiere la creación de un método nuevo en OperadorBinario, que estará vacío inicialmente.

El proceso que seguimos ahora es similar al seguido hasta ahora, de modo que finalmente nuestro código queda del siguiente modo:

calculadora/tests/operaciones/OperadorBinarioTest.php <?php namespace calculadora\operaciones; require '../vendor/autoload.php'; /** * Generated by PHPUnit_SkeletonGenerator on 2015-12-30 at 16:08:19. */ class OperadorBinarioTest extends \PHPUnit_Framework_TestCase { /** * @var OperadorBinario */ protected $operadorBinario; /** * Sets up the fixture, for example, opens a network connection. * This method is called before a test is executed. */ protected function setUp() { $this->operadorBinario = new OperadorBinario; } /** * Tears down the fixture, for example, closes a network connection. * This method is called after a test is executed. */ protected function tearDown() { } /** * @dataProvider providerSumaDosNumerosPositivos */ public function testSumaDosNumerosPositivos($num1,$num2,$numeroEsperado){ $numeroObtenido = $this->operadorBinario->suma($num1,$num2); $this->assertEquals($numeroEsperado,$numeroObtenido); } /** * * @param type $num1 * @param type $num2 * @dataProvider providerRestaDosNumerosPositivos */ public function testRestaDosNumerosPositivos($num1,$num2,$numeroEsperado){ $numeroObtenido = $this->operadorBinario->resta($num1,$num2); $this->assertEquals($numeroEsperado,$numeroObtenido); } public function providerSumaDosNumerosPositivos(){ return array( '2 + 2 = 4' => array(2,2,4), '5 + 2 = 7' => array(5,2,7) ); } public function providerRestaDosNumerosPositivos(){ return array( '5 - 3 = 2' => array(5,3,2), '3 - 5 = -2' => array(3,5,-2) ); } }
calculadora/operaciones/OperadorBinario.php <?php /** * Description of OperadorBinario * * @author mauri */ namespace calculadora\operaciones; class OperadorBinario { public function suma($num1, $num2){ return $num1 + $num2; } public function resta($num1, $num2){ return $num1 - $num2; } }

Unos pocos tests después

Supongamos que hemos añadido unos cuantos tests más, refactorizado cuando ha hecho falta, etc. El código resultante queda así:

calculadora/operaciones/OperadorBinario.php <?php /** * Description of OperadorBinario * * @author mauri */ namespace calculadora\operaciones; class OperadorBinario { const SUMA = 0; const RESTA = 1; const MULTIPLICACION = 2; const DIVISION = 3; public function operacion($operando1,$operando2,$operacion){ switch ($operacion){ case OperadorBinario::SUMA: return $this->suma($operando1,$operando2); case OperadorBinario::RESTA: return $this->resta($operando1,$operando2); case OperadorBinario::MULTIPLICACION: return $this->multiplicacion($operando1,$operando2); case OperadorBinario::DIVISION: return $this->division($operando1,$operando2); } } public function suma($num1, $num2){ return $num1 + $num2; } public function resta($num1, $num2){ return $num1 - $num2; } public function multiplicacion($num1, $num2){ return $num1 * $num2; } public function division($num1, $num2){ return $num1 / $num2; } }
calculadora/tests/operaciones/OperadorBinarioTest.php <?php namespace calculadora\operaciones; require '../vendor/autoload.php'; /** * Generated by PHPUnit_SkeletonGenerator on 2015-12-30 at 16:08:19. */ class OperadorBinarioTest extends \PHPUnit_Framework_TestCase { /** * @var OperadorBinario */ protected $operadorBinario; /** * Sets up the fixture, for example, opens a network connection. * This method is called before a test is executed. */ protected function setUp() { $this->operadorBinario = new OperadorBinario; } /** * Tears down the fixture, for example, closes a network connection. * This method is called after a test is executed. */ protected function tearDown() { } /** * * @param type $numeros * @param type $operacion * @param type $resultadoEsperado * @dataProvider providerOperacionDosNumerosPositivos */ public function testOperacionDosNumerosPositivos($numeros,$operacion,$resultadoEsperado){ $resultadoObtenido = $this->operadorBinario->operacion($numeros[0], $numeros[1], $operacion); $this->assertEquals($resultadoEsperado,$resultadoObtenido); } public function providerOperacionDosNumerosPositivos(){ return array( '2 + 2 = 4' => array([2,2],OperadorBinario::SUMA,4), '2 + 5 = 7' => array([2,5],OperadorBinario::SUMA,7), '5 - 3 = 2' => array([5,3],OperadorBinario::RESTA,2), '3 - 5 = -2'=> array([3,5],OperadorBinario::RESTA,-2), '2 * 5 = 10'=> array([2,5],OperadorBinario::MULTIPLICACION,10), '4 * 8 = 32'=> array([4,8],OperadorBinario::MULTIPLICACION,32), '10 / 2 = 5'=> array([10,2],OperadorBinario::DIVISION,5), '30 / 3 = 10' => array([30,3],OperadorBinario::DIVISION,10) ); } } snippet

Más tests...

Si tenemos en cuenta que podemos elegir la prueba de aceptación que queramos, la siguiente prueba puede ser esta: Límite Superior =100. La prueba nos ayuda a definir unos cuantos tests:

  • Límite Superior =100
    • Lim.Sup. = 100: 100 + 0 = 100
    • Lim.Sup. = 100: 0 + 100 = 100
    • Lim.Sup. = 100: 100 + 1 -> ERROR
    • Lim.Sup. = 100: 101 + 0 -> ERROR
    • Lim.Sup. = 100: 40 + 70 -> ERROR

Modificamos nuestro código de pruebas. La clase OperadorBinario debe tener un máximo y un mínimo, así que redefinimos los tests y la clase:

calculadora/tests/operaciones/OperadorBinarioTest.php <?php namespace calculadora\operaciones; require '../vendor/autoload.php'; /** * Generated by PHPUnit_SkeletonGenerator on 2015-12-30 at 16:08:19. */ class OperadorBinarioTest extends \PHPUnit_Framework_TestCase { /** * @var OperadorBinario */ protected $operadorBinario; /** * Sets up the fixture, for example, opens a network connection. * This method is called before a test is executed. */ protected function setUp() { $this->operadorBinario = new OperadorBinario; } /** * Tears down the fixture, for example, closes a network connection. * This method is called after a test is executed. */ protected function tearDown() { } /** * * @param type $numeros * @param type $operacion * @param type $resultadoEsperado * @dataProvider providerOperacionDosNumerosPositivos */ public function testOperacionDosNumerosPositivos($numeros,$operacion,$resultadoEsperado){ $resultadoObtenido = $this->operadorBinario->operacion($numeros[0], $numeros[1], $operacion); $this->assertEquals($resultadoEsperado,$resultadoObtenido); } /** * * @param type $operandos * @param type $operacion * @param type $limiteSuperior * @return type * @dataProvider providerOperacionDosNumerosConLimiteSuperiorSuperado */ public function testOperacionDosNumerosConLimiteSuperiorSuperado($operandos,$operacion,$limiteSuperior){ try{ $this->operadorBinario->setLimiteSuperior($limiteSuperior); $this->operadorBinario->operacion($operandos[0],$operandos[1],$operacion); } catch (\Exception $ex) { $this->assertTrue(true); return; } $this->fail("La operación se ha podido realizar a pesar de haber superado el límite superior"); } public function providerOperacionDosNumerosPositivos(){ return array( '2 + 2 = 4' => array([2,2],OperadorBinario::SUMA,4), '2 + 5 = 7' => array([2,5],OperadorBinario::SUMA,7), '5 - 3 = 2' => array([5,3],OperadorBinario::RESTA,2), '3 - 5 = -2'=> array([3,5],OperadorBinario::RESTA,-2), '2 * 5 = 10'=> array([2,5],OperadorBinario::MULTIPLICACION,10), '4 * 8 = 32'=> array([4,8],OperadorBinario::MULTIPLICACION,32), '10 / 2 = 5'=> array([10,2],OperadorBinario::DIVISION,5), '30 / 3 = 10' => array([30,3],OperadorBinario::DIVISION,10) ); } public function providerOperacionDosNumerosConLimiteSuperiorSuperado(){ return array( 'Lim.Sup=100: 100 + 1 -> ERROR' => array([100,1],OperadorBinario::SUMA,100), 'Lim.Sup=100: 101 + 0 -> ERROR' => array([101,0],OperadorBinario::SUMA,100), 'Lim.Sup=100: 40 + 61 -> ERROR' => array([40,61],OperadorBinario::SUMA,100) ); } }

A la clase OperadorBinario le añadimos un método para definir un límite superior.

calculadora/operaciones/OperadorBinario.php <?php /** * Description of OperadorBinario * * @author mauri */ namespace calculadora\operaciones; class OperadorBinario { const SUMA = 0; const RESTA = 1; const MULTIPLICACION = 2; const DIVISION = 3; protected $limiteSuperior; public function operacion($operando1,$operando2,$operacion){ switch ($operacion){ case OperadorBinario::SUMA: return $this->suma($operando1,$operando2); case OperadorBinario::RESTA: return $this->resta($operando1,$operando2); case OperadorBinario::MULTIPLICACION: return $this->multiplicacion($operando1,$operando2); case OperadorBinario::DIVISION: return $this->division($operando1,$operando2); } } public function suma($num1, $num2){ return $num1 + $num2; } public function resta($num1, $num2){ return $num1 - $num2; } public function multiplicacion($num1, $num2){ return $num1 * $num2; } public function division($num1, $num2){ return $num1 / $num2; } public function setLimiteSuperior($limiteSuperior){ $this->limiteSuperior = $limiteSuperior; } }

De momento los tests fallan:

Aun no hemos creado el código necesario para pasar los test

Después de esto, añadimos el código necesario para cubrir los tests con el código mínimo:

calculadora/operaciones/OperacionBinaria.php <?php /** * Description of OperadorBinario * * @author mauri */ namespace calculadora\operaciones; class OperadorBinario { const SUMA = 0; const RESTA = 1; const MULTIPLICACION = 2; const DIVISION = 3; protected $limiteSuperior = \NULL; public function operacion($operando1,$operando2,$operacion){ if (($this->limiteSuperior != \NULL) && (($operando1 > $this->limiteSuperior) || ($operando2 > $this->limiteSuperior))){ throw new \Exception("Se ha superado el límite superior"); } $resultado = 0; switch ($operacion){ case OperadorBinario::SUMA: $resultado = $this->suma($operando1,$operando2); break; case OperadorBinario::RESTA: $resultado = $this->resta($operando1,$operando2); break; case OperadorBinario::MULTIPLICACION: $resultado = $this->multiplicacion($operando1,$operando2); break; case OperadorBinario::DIVISION: $resultado = $this->division($operando1,$operando2); break; } if (($this->limiteSuperior != \NULL) && ($resultado > $this->limiteSuperior)){ throw new \Exception("Se ha superado el límite superior"); } else { return $resultado; } } public function suma($num1, $num2){ return $num1 + $num2; } public function resta($num1, $num2){ return $num1 - $num2; } public function multiplicacion($num1, $num2){ return $num1 * $num2; } public function division($num1, $num2){ return $num1 / $num2; } public function setLimiteSuperior($limiteSuperior){ $this->limiteSuperior = $limiteSuperior; } }

Después de esto, todos los tests están en verde:

Después "del verde" viene la factorización.

El código es redundante, así que vamos a refactorizar. La clase OperadorBinario, queda del siguiente modo:

calculadora/operaciones/OperadorBinario.php <?php /** * Description of OperadorBinario * * @author mauri */ namespace calculadora\operaciones; class OperadorBinario { const SUMA = 0; const RESTA = 1; const MULTIPLICACION = 2; const DIVISION = 3; protected $limiteSuperior = \NULL; public function operacion($operando1,$operando2,$operacion){ $resultadoSinValidarLimites = $this->operacionSinValidarLimites($operando1,$operando2,$operacion); return $this->validarLimites($resultadoSinValidarLimites); } protected function operacionSinValidarLimites($operando1, $operando2, $operacion){ switch ($operacion){ case OperadorBinario::SUMA: return $this->suma($operando1,$operando2); case OperadorBinario::RESTA: return $this->resta($operando1,$operando2); case OperadorBinario::MULTIPLICACION: return $this->multiplicacion($operando1,$operando2); case OperadorBinario::DIVISION: return $this->division($operando1,$operando2); } } protected function validarLimites($resultadoOperacion){ if (($this->limiteSuperior != \NULL) &&($resultadoOperacion > $this->limiteSuperior)){ throw new \Exception("Se ha superado el límite superior"); } else { return $resultadoOperacion; } } public function suma($num1, $num2){ return $num1 + $num2; } public function resta($num1, $num2){ return $num1 - $num2; } public function multiplicacion($num1, $num2){ return $num1 * $num2; } public function division($num1, $num2){ return $num1 / $num2; } public function setLimiteSuperior($limiteSuperior){ $this->limiteSuperior = $limiteSuperior; } }

S.O.L.I.D. son 5 principios de programación que ayudarán a que nuestro código no se convierta a largo plazo en una masa amorfa de cosas que funcionan de pura chiripa, y de gran fagilidad, que no admite cambios sin dar problemas. Dichos principos se pueden resumir del siguiente modo:

  • Single Responsibility: cada clase tiene una única responsabilidad, y solo hay un motivo para cambiarla.
  • Open/Close: los cambios en una clase no deberían extenderse a otras clases. La forma de seguir este principio es usar la generalización siempre que se pueda.
  • Liskov Substitution: Nuestros diseños deben permitir que las clases hijas puedan ser tratadas como las clases padre. Es decir, cuando más general sea el uso de nuestras clases, mejor.
  • Interface Segregation: es preferible muchas clases con pocos métodos a pocas interfaces con muchos métodos
  • Dependency Inversion: Las clases concretas deben depender de generalizaciones, no de otras clases concretas.

Si nos fijamos, el programa que hemos escrito incumple el primer principio: Una sola responsabilidad. De hecho hace dos cosas:

  • Realiza operaciones aritméticas
  • Controla los límites superior e inferior

Para seguir refactorizando adecuadamente, deberíamos separar en dos clases. Existen varias alternativas, pero siempre deben seguir los principios SOLID.

Puedes consultar más sobre SOLID en http://devexperto.com/principio-responsabilidad-unica

Y así, seguimos...

Si seguimos adelante con el proceso, segirá surgiendo nustro software como hemos visto hasta ahora. Es decir:

  • Rojo: Pruebas escritas no superadas.
  • Verde: Código escrito y pruebas superadas.
  • Refactorización: Reescribir nuestro software para que aumentar su calidad.

Seguiremos añadiendo nuevas pruebas de aceptación y a partir de ellas iremos creando nuestros tests unitarios, hasta cubrir todas las pruebas.

Actividad 1. Crea de nuevo el proyecto de conversión de medidas que creaste en el tema anterior. Sin embargo ahora deberás emplear la metodología TDD para su desarrollo. Ten en cuenta que las pruebas de aceptación son las siguientes:

  • 1 inch = 0,3937 cm
  • 12 inch = 4,7244 cm
  • 12 cm = 30,48 inc
  • 0 cm = 0 inch
  • 0,5 cm = 0,1968 inch
  • Límite superior 40 inch: 41 inch = ERROR
  • Límite superior 40 inch: 104 cm = ERROR
  • Límite superior 100 cm: 101 cm = ERROR

Entrega una archivo llamado Act1-phpunit.zip que contenga lo siguiente:

  • El proyecto completo, incluyendo las pruebas.
  • Una captura donde se puedan ver las pruebas en rojo.
  • Una captura donde se puedan ver las pruebas en verde.
  • Un archivo de texto donde expliques las diferentes refactorizaciones que has llevado a cabo tras poner las pruebas en verde.

Objetos MOCK

En https://phpunit.de/manual/current/en/test-doubles.html se describe cómo utilizar los objetos MOCK. Daré aquí algunas indicaciones.

Siguiendo con nuestro ejemplo de la calculadora, supongamos que queremos poder realizar operaciones a partir de cadenas de texto como "3 + 4" o "5 * 7". Y puede que más adelante podamos resolver también cosas como "suma(5,3)" o "sumar 4 y 3".

Vamos a suponer también que ya llevamos trabajando algo de tiempo en el problema, de forma que hemos decidido que la mejor manera de hacer esto es utilizando una clase llamada ParseadorOperacionTextoPlano que nos devuelve los dos operandos y la operación a llevar a cabo. Es decir, de la cadena "3 + 4" nos devolverá un array con el siguiente contenido: [3,4,OperadorBinario::SUMA].

En nuestra clase OperadorBinario contaremos con un par de nuevos métodos:

calculadora/operaciones/OperadorBinario.php <?php /** * Description of OperadorBinario * * @author mauri */ namespace calculadora\operaciones; require 'vendor/autoload.php'; use \calculadora\parseadores\ParseadorOperacionBinariaTextoPlano; class OperadorBinario { const SUMA = 0; const RESTA = 1; const MULTIPLICACION = 2; const DIVISION = 3; protected $limiteSuperior = \NULL; /** @var ParseadorOperacionBinariaTextoPlano */ protected $parseadorOperacionBinariaTextoPlano = \NULL; public function operacion($operando1,$operando2,$operacion){ $resultadoSinValidarLimites = $this->operacionSinValidarLimites($operando1,$operando2,$operacion); return $this->validarLimites($resultadoSinValidarLimites); } protected function operacionSinValidarLimites($operando1, $operando2, $operacion){ switch ($operacion){ case OperadorBinario::SUMA: return $this->suma($operando1,$operando2); case OperadorBinario::RESTA: return $this->resta($operando1,$operando2); case OperadorBinario::MULTIPLICACION: return $this->multiplicacion($operando1,$operando2); case OperadorBinario::DIVISION: return $this->division($operando1,$operando2); } } protected function validarLimites($resultadoOperacion){ if (($this->limiteSuperior != \NULL) &&($resultadoOperacion > $this->limiteSuperior)){ throw new \Exception("Se ha superado el límite superior"); } else { return $resultadoOperacion; } } public function suma($num1, $num2){ return $num1 + $num2; } public function resta($num1, $num2){ return $num1 - $num2; } public function multiplicacion($num1, $num2){ return $num1 * $num2; } public function division($num1, $num2){ return $num1 / $num2; } public function setLimiteSuperior($limiteSuperior){ $this->limiteSuperior = $limiteSuperior; } public function setParseadorOperacionBinariaTextoPlano($parseadorOperacionBinariaTextoPlano){ $this->parseadorOperacionBinariaTextoPlano = $parseadorOperacionBinariaTextoPlano; } public function operacionBinariaTextoPlano($operacionBinariaTextoPlano){ if ($this->parseadorOperacionBinariaTextoPlano != \NULL){ $operacion = $this->parseadorOperacionBinariaTextoPlano->parsearOperacionBinariaTextoPlano($operacionBinariaTextoPlano); return $this->operacion($operacion[0], $operacion[1], $operacion[2]); } else { throw new Exception("No se ha definido un parseador para operaciones en texto plano."); } } }

Por otra parte tenemos nuestra nueva clase ParseadorOperacionBinariaTextoPlano:

calculadora/parseadores/ParseadorOperacionBinariaTextoPlano.php <?php namespace calculadora\parseadores; /** * Description of ParseadorOperacionTextoPlano * * @author mauri */ class ParseadorOperacionBinariaTextoPlano { public function parsearOperacionBinariaTextoPlano($operacionTextoPlano){ } }

Como vemos, esta clase está incompleta.

Para no tener problemas con la carga de la nueva clase, vamos a modificar el archivo composer.json

calculadora/composer.json { "require-dev":{ "phpunit/phpunit":"5.1.3" }, "autoload":{ "psr-4":{ "calculadora\\operaciones\\":"operaciones/", calculadora\\parseadores\\":"parseadores/" } } }

Y después actualizamos composer, para que se cargen nuestras clases (ya sea desde NetBeans o desde la terminal con composer dump-autoload.

Necesitamos probar OperacionBinaria

El problema que tenemos es que antes de seguir debemos probar los nuevos métodos de OperacionBinaria, pero contamos con los siguientes problema:

  • La clase ParseadorOperacionBinariaTextoPlano está incompleta. En TDD debemos ir dando pequeños pasos. No me conviene implementar de golpe la clase para poder probar otra.
  • Una de las reglas de las pruebas unitarias es que sean atómicas. Deben probar solamente una cosa. Si estamos probando más de una cosa, entonces estamos en una prueba de integración, y estas tocan hasta que hemos probado con pruebas unitarias los diferentes componentes.

En ocasiones el problema es otro. Por ejemplo, podemos necesitar trabajar con una base de datos que aún no está disponible, o que no contiene datos (porque aún no disponemos de ellos). O bien contamos con un servicio en red que podría tardar lo suyo, o que está temporalmente offline, etc.

Para superar este problema usamos objetos MOCK. Vamos a ver una prueba:

calculadora/tests/operaciones/OperadorBinarioTest.php <?php namespace calculadora\operaciones; require '../vendor/autoload.php'; /** * Generated by PHPUnit_SkeletonGenerator on 2015-12-30 at 16:08:19. */ class OperadorBinarioTest extends \PHPUnit_Framework_TestCase { /** * @var OperadorBinario */ protected $operadorBinario; /** * Sets up the fixture, for example, opens a network connection. * This method is called before a test is executed. */ protected function setUp() { $this->operadorBinario = new OperadorBinario; } /** * Tears down the fixture, for example, closes a network connection. * This method is called after a test is executed. */ protected function tearDown() { } /** * * @param type $numeros * @param type $operacion * @param type $resultadoEsperado * @dataProvider providerOperacionDosNumerosPositivos */ public function testOperacionDosNumerosPositivos($numeros,$operacion,$resultadoEsperado){ $resultadoObtenido = $this->operadorBinario->operacion($numeros[0], $numeros[1], $operacion); $this->assertEquals($resultadoEsperado,$resultadoObtenido); } /** * * @param type $operandos * @param type $operacion * @param type $limiteSuperior * @return type * @dataProvider providerOperacionDosNumerosConLimiteSuperiorSuperado */ public function testOperacionDosNumerosConLimiteSuperiorSuperado($operandos,$operacion,$limiteSuperior){ try{ $this->operadorBinario->setLimiteSuperior($limiteSuperior); $this->operadorBinario->operacion($operandos[0],$operandos[1],$operacion); } catch (\Exception $ex) { return; } $this->fail("La operación se ha podido realizar a pesar de haber superado el límite superior"); } public function invocarMetodo(&$objeto, $nombreMetodo, array $parametros = array()) { $reflejo = new \ReflectionClass(get_class($objeto)); $metodo = $reflejo->getMethod($nombreMetodo); $metodo->setAccessible(true); return $metodo->invokeArgs($objeto, $parametros); } public function testValidarLimites(){ $limiteSuperior=100; $this->operadorBinario->setLimiteSuperior($limiteSuperior); try{ $this->invocarMetodo($this->operadorBinario, "validarLimites",[101]); } catch (\Exception $ex) { return; } $this->fail("Se ha validado un límite superior."); } public function testOperacionDosNumerosTextoPlano(){ $resultadoEsperado = 5; $parseador = $this->getMockBuilder("calculadora\parseadores\ParseadorOperacionBinariaTextoPlano") ->getMock(); $parseador->expects($this->once())->method('parsearOperacionBinariaTextoPlano')->will($this->returnValue([2,3,OperadorBinario::SUMA])); $this->operadorBinario->setParseadorOperacionBinariaTextoPlano($parseador); $resultadoObtenido = $this->operadorBinario->operacionBinariaTextoPlano("2+3"); $this->assertEquals($resultadoEsperado,$resultadoObtenido); } public function providerOperacionDosNumerosPositivos(){ return array( '2 + 2 = 4' => array([2,2],OperadorBinario::SUMA,4), '2 + 5 = 7' => array([2,5],OperadorBinario::SUMA,7), '5 - 3 = 2' => array([5,3],OperadorBinario::RESTA,2), '3 - 5 = -2'=> array([3,5],OperadorBinario::RESTA,-2), '2 * 5 = 10'=> array([2,5],OperadorBinario::MULTIPLICACION,10), '4 * 8 = 32'=> array([4,8],OperadorBinario::MULTIPLICACION,32), '10 / 2 = 5'=> array([10,2],OperadorBinario::DIVISION,5), '30 / 3 = 10' => array([30,3],OperadorBinario::DIVISION,10) ); } public function providerOperacionDosNumerosConLimiteSuperiorSuperado(){ return array( 'Lim.Sup=100: 100 + 1 -> ERROR' => array([100,1],OperadorBinario::SUMA,100), 'Lim.Sup=100: 101 + 0 -> ERROR' => array([101,0],OperadorBinario::SUMA,100), 'Lim.Sup=100: 40 + 61 -> ERROR' => array([40,61],OperadorBinario::SUMA,100) ); } }

Como se puede ver, uno de los parámetros empleados al crear el objeto Mock es el número de veces que va a ser llamado un cierto método, en la línea siguiente:

$parseador->expects($this->once())->method('parsearOperacionBinariaTextoPlano')->will($this->returnValue([2,3,OperadorBinario::SUMA]));

Con $this->once() estamos indicando que el método va a ser ejecutado una única vez. Si tratamos de ejecutarlo más veces se producirá un error.

Existen más matchers (así se llama el parámetro pasado al método expects para indicar el número de llamadas esperadas). Se pueden consultar aquí: https://phpunit.de/manual/current/en/test-doubles.html#test-doubles.mock-objects.tables.matchers. Sus nombres son bastante explicativos:

  • any()
  • never()
  • atLeastOnce()
  • once()
  • exactly($numeroVeces)
  • at($enesimaVez)

Los objetos MOCK utilizan el término stub method y mock method:

  • stub method: método contenido en un objeto mock que devuelve null (este es el comportamiento por defecto de un objeto mock para todos sus métodos). En el test anterior, redefinimos el "stub method" para que hiciera lo que queremos.
  • mock method: método de un objeto mock que hace exactamente lo mismo que el método de la clase original.

Existen mecanismos para decidir si un método será un "stub method" o un "mock method":

Todos los métodos como "stub methods"

$objetoMock = $this->getMockBuilder(\nombre\ClaseEjemplo)->getMock();

Esto produce un objeto Mock de la clase "ClaseEjemplo" donde:

  • Todos los métodos son "stub"
  • Todos los métodos devuelven null
  • Se pueden sobreescribir igual que en el test mostrado anteriormente

Todos los métodos como "mock methods"

$objetoMock = $this->getMockBuilder(\nombre\ClaseEjemplo) ->setMethods(null) ->getMock();

Esto produce un objeto Mock de la clase "ClaseEjemplo" donde:

  • Todos los métodos son mock
  • Ejecutan el código original
  • No se pueden sobreescribir

Solo algunos métodos son "mock methods" y el resto son "stub methods"

$objetoMock = $this->getMockBuilder(\nombre\ClaseEjemplo) ->setMethods(array('metodo1', 'metodo2')) ->getMock();

Esto produce un objeto Mock de la clase "ClaseEjemplo" donde:

  • Los métodos que han sido identificados (metodo1 y metodo2):
    • metodo1 y metodo2 son mock
    • Ejecutan el código original
    • No se pueden sobreescribir
  • Los métodos que no han sido identificados (el resto de métodos de la clase ClaseEjemplo diferentes a metodo1 y metodo2):
    • Son métodos stub
    • Devuelven null
    • Se pueden sobreescribir

Cuándo usar métodos mock y métodos stub

En ocasiones un método puede hacer algo que no nos venga bien para nuestras pruebas (por ejemplo, un cierto "exit" que interrumpe la prueba). En tal caso, podemos utilizar objetos mock, y definir como métodos stub aquellos que no nos vengan bien y como métodos mock aquellos que necesitemos. De esta forma no tenemos que toquetear nuestro código.

Actividad 2. Sigues trabajando en tu proyecto de conversión de unidades. Ahora vamos a hacer la siguiente suposición. Imagina que existe una clase llamada "EquivalenciaUnidades". Esta clase tiene un método llamado "equivalencia":

public function getEquivalencia($from,$to){ // Devuelve la equivalencia entre $from y $to. Por ejemplo: // getEquivalencia("km","m") devuelve 1000 }

El problema que tenemos es que todavía no ha sido escrito el código necesario en dicho método. Pero afortunadamente podemos crear un objeto "Mock" que simule el comportamiento de dicho método, sin necesidad de escribirlo aún.

Lo que tienes que hacer en la clase conversora

Deberás añadir a tu clase conversora los siguientes elementos:

  1. un nuevo atributo llamado "$equivalenciaUnidades"
  2. un método llamado "setEquivalenciaUnidades" que tome un objeto de tipo "EquivalenciasUnidades" y lo asigne al atributo "$equivalenciasUnidades"
  3. El el método donde realizas la conversión, deberás obtener el factor de conversión a través del objeto "EquivalenciaUnidades".

Por ejemplo, tu clase conversora podría quedar más o menos así:

class Conversor { $protected equivalenciasUnidades; public function convertir($valor,$from,$to){ return $valor/$this->equivalenciasUnidades->getEquivalencia($from,$to); } public function setEquivalenciaUnidades($equivalenciaUnidades){ $this->equivalenciaUnidades = $equivalenciaUnidades; } }

Lo que tienes que hacer en la clase EquivalenciaUnidades

Lo que tienes que hacer es lo siguiente:

  1. Crear la clase.
  2. Crear el método "getEquivalencia", que está vacío

Lo que tienes que hacer con los tests

Modifica los tests que hiciste anteriormente para que se ajusten a la nueva estructura de la clase Conversor. Deberás asignar al conversor en cada test un objeto de tipo "EquivalenciaUnidades". Dado que el objeto "EquivalenciasUnidades" no está aún escrito, utiliza un objeto Mock para simular su comportamiento.

Entrega los siguientes items en un archivo comprimido llamado Act2-phpunit.zip:

  • Una captura donde se puedan ver los tests en rojo.
  • Otra captura donde se puedan ver los tests en verde.
  • El código fuente del proyecto

Métricas del software

Las métricas del software nos permiten hacernos una idea de cómo va nuestro software, y nos ayuda a tomar decisiones inteligentes.

Existen muchas y diversas métricas. Yo me voy a centrar en métricas centradas en la complejidad del software. Es decir, cómo de difícil va a ser mantener nuestro código. El siguiente documento muestra algo de teoría sobre las métricas de la complejidad del software.

Dos medidas interesantes son:

  • La complejidad ciclomática (referida en el documento anteriro como "número ciclomático"). Podemos ver valores recomendados para la complejidad ciclomática en http://joaquinoriente.com/2012/11/15/complejidad-ciclomatica-nuestro-codigo-es-facil-de-mantener-y-probar/
  • El número de lineas repetidas. Un alto porcentaje de código repetido es síntoma de un mal diseño. Un buen diseño evita los códigos repetidos. Las repeticiones (tipo copy/paste) provoca los siguientes problemas:
    • Aumenta innecesariamente el número de líneas de código (a más líneas de código más complejo es el mantenimiento, y más costoso).
    • Dispara los costes (si hay que cambiar ese código repetido... hay que cambiarlo en muchos sitios).
    • Aumenta los riesgos (hay que buscar todas las repeticiones y si se nos olvida cambiarlo en algún sitio… el software acaba siendo incoherente).

Qué hacer con las medidas

El proceso de trabajo con las medidas es el siguiente:

  1. Obtención de las medidas mediante herramientas matemáticas, que den medidas objetivas y no simples interpretaciones.
  2. Análisis de resultados e interpretación que nos permita valorar la razón de los resultados.
  3. Retroalimentación en forma de recomendaciones y decisiones a tomar sobre el código.

Este proceso debería repetirse hasta obtener una medida aceptable, si no queremos (a buen seguro) contraer un Deuda Técnica.

La Deuda Técnica

Las metodologías ágiles permiten ganar velocidad en el desarrollo. Pero si las cosas se hacen mal, con una metodología ágil, la velocidad en alcanzar el desastre también es mayor (ver https://www.scrumalliance.org/community/articles/2010/december/the-land-that-scrum-forgot.

La deuda técnica dice que cuando los objetivos son los erróneos (acabar cuanto antes, por ejemplo), se contrae una deuda técnica que se terminará pagando de algún modo:

  • Lo paga el cliente
  • Lo paga el proveedor y los desarrolladores

La forma de evitar la Deuda Técnica es que el equipo de desarrollo esté centrado en la calidad del software, y no en cosas como los plazos de entrega.

Herramientas

En PHP existen varios proyectos que nos ayudan a comprobar la complejidad de nuestro software y a medir de forma objetiva la calidad de nuestro software. Estos proyectos son:

  • PHPCPD (PHP Copy/Paste Detector).
  • PHPLOC (PHP Lines Of Code).

La relación entre la complejidad del software y la refactorización

Las refactorizaciones pueden verse como un tipo de mantenimiento preventivo, cuyo objetivo es disminuir la complejidad del software en anticipación a los incrementos de complejidad que los cambios pudieran traer. Ver más en http://www.javiergarzas.com/2011/02/refactoring-1.html.

Actividad 3. Partiendo de la actividad anterior (2), calcula la complejidad ciclomática del código y los fragmentos de código repetidos que encuentres en la carpeta modelo/.

Toma dos capturas donde se puedan ver los resultados obtenidos y guárdalas en un archivo llamado Act3-phpunit.zip

Pruebas de integración

Con PHPUnit podemos crear nuestras pruebas de integración si combinamos en nuestros tests varios componentes diferentes de nuestro código en lugar de usar objetos mock.

Es importante tener claro que los tests unitarios son atómicos, de forma que si interviene más de un componente en un test, ya está dejando de ser un test unitario.

Actividad 4. Descarga el proyecto de calculadora TDD con validacion

Entre las pruebas, hay un test llamado ValidadorLimiteSuperiorTest que utiliza un objeto Mock para la clase OperadorBinario. Partiendo de esta prueba, escribe una prueba de integración llamada ValidadorLimiteSuperiorIntegracionTest que pruebe el funcionamiento conjunto de las clases OperadorBinario y ValidadorLimiteSuperior sin el uso de objetos Mock.

Entrega la actividad en un archivo llamado Act4-phpunit.zip

La pruebas de sistema

Para hacer las pruebas de sistema vamos a utilizar Selenium para poder automatizar las acciones que ocurren en un navegador.

Cómo funciona Selenium

Para trabajar con Selenium necesitamos como mínimo dos componentes:

  • Un servidor Selenium. En la página de descargas podemos encontrarlo en la sección "Selenium Standalone Server".
  • Un driver para poder comunicarnos con el servidor Selenium y enviarle comandos. Este driver aparece en forma de librería en un cierto lenguaje (en nuestro caso PHP).

A estos componentes podemos añadir otros dos más, que no son necesarios pero nos harán la vida más agradable:

  • Una librería para automatizar pruebas (en nuestro caso PHPUnit)
  • Un IDE (en nuestro caso NetBeans)

Vamos a suponer que tenemos un script que hace una prueba sobre nuestra aplicación usando un webdriver para Selenium. El proceso que vamos a seguir al ejecutar un test con Selenium es el siguiente:

  1. El webdriver establece una conexión con el servidor Selenium
  2. El servidor Selenium hace lo siguiente:
    • Crea una sesión para la solicitud
    • Lanza el navegador deseado
    • Carga en el navegador las librerías JavaScript necesarias para poder llevar a cabo las acciones que se pidan desde el script (acciones en lenguaje Salenese)
  3. El webdriver realiza una traducción de las acciones escritas en un test (en nuestro caso en PHP) a Salenese, y se las envía al servidor Selenium.
  4. El servidor interpreta las acciones Salenese y planifica cada script JavaScript necesario para lanzar la acción correspondiente (como por ejemplo, rellenar un campo de texto con el texto "Hola mundo").
  5. A partir de este momento, el servidor Selenium actúa de intermediario entre el navegador lanzado y el servidor web que aloja la aplicación que estamos probando. De esta forma, se creará una conversación entre el navegador y al aplicación, en la que el servidor Selenium añade añade las acciones pedidas en el script.
  6. Finalmente, una vez que el servidor Selenium tenga la respuesta definitiva desde el servidor web, enviará la respuesta al script que inició el proceso.
Funcionamiento del servidor Selenium

Instalación del servidor Selenium

Para instalar el servidor Selenium primero lo descargamos desde http://docs.seleniumhq.org/download/. Será un archivo llamado selenium-server-standalone-<version>.jar.

Podemos ejecutar directamente el servidor con el comando java -jar selenium-server-standalone-<version>.jar. También podemos mover el archivo selenium-server-standalone-<version>.jar a /usr/local/bin/ y crear un alias para el comando.

Para hacer esto último seguimos los siguientes pasos:

1) Como usuario root, mover el servidor a /usr/local/bin/

#> mv selenium-server-standalone-<version>.jar /usr/local/bin/

2) Como usuario no root, editamos el archivo .bashrc del usuario (no root), y añadimos la línea resaltada en blanco:

/home/mauri/.bashrc # .bashrc # Source global definitions if [ -f /etc/bashrc ]; then . /etc/bashrc fi # Uncomment the following line if you don't like systemctl's auto-paging feature: # export SYSTEMD_PAGER= # User specific aliases and functions alias selenium="java -jar /usr/local/bin/selenium-server-standalone-2.48.2.jar"

3) A partir de ahora, cada vez que iniciemos sesión se creará el alias, de forma que bastará con ejecutar el comando "selenium" y lanzaremos el servidor. No es preciso que seamos "root" para ejecutar el servidor Selenium. Si no tenemos ganas de cerrar sesión y volver a empezar, podemos ejecutar el siguiente comando:

$> source ~/.bashrc $> selenium

Tras ejecutar el servidor obtendremos una salida como la siguiente:

Salida obtenida al ejecutar el servidor Selenium.

Instalar el webdriver

Para poder interactuar con el servidor Selenium necesitamos un webdriver. En concreto vamos a utilizar uno llamado php-webdriver desarrollado por Facebook.

Para instalar en nuestro proyecto php-webdriver, vamos a utilizar Composer. Si seguimos trabajando sobre el proyecto de la calculadora:

calculadora/composer.json { "require-dev":{ "phpunit/phpunit":">=5.1.3-stable", "phploc/phploc":">=2.1.5-stable", "sebastian/phpcpd":">=2.0.2-stable", "facebook/webdriver": "dev-master" }, "autoload":{ "psr-4":{ "calculadora\\operaciones\\":"operaciones/", "calculadora\\parseadores\\":"parseadores/" } } }

Preparando los directorios

En primer lugar vamos a preparar todo en NetBeans para que funcione de manera automática. Lo primero es crear una nueva carpeta llamada calculadora/system-tests. En esta carpeta ubicaremons nuestros tests para Selenium.

Lo siguiente es configurar el proyecto para que la carpeta calculadora/system-tests sea la carpeta de para Selenium. Para ello, seguimos los siguientes pasos:

  1. Hacemos clic derecho sobre el proyecto.
  2. Abrimos las propiedades del proyecto y nos vamos a la sección "Selenium Testing".
  3. Hacemos clic en el botón "Add Folder" y elegimos la carpeta recién creada calculadora/system-tests

Con la configuración anterior, los tests que creemos para Selenium se van a almacenar en calculadora/system-tests. Pero en realidad nos vamos a apoyar en PHPUnit para crearlos. Lo que esto quiere decir, es que debemos configurar el proyecto para que PHPUnit ejecute automáticamente estos tests cuando lancemos los tests. Para ello, debemos seguir los siguientes pasos:

  1. Hacemos clic derecho sobre el proyecto.
  2. Abrimos la sección "Testing"
  3. Hacemos clic en el botón "Add Folder" y elegimos la carpeta calculadora/system-tests.

Creando un test con PHPUnit y php-webdriver

Los tests no son muy diferentes de los creados con PHPUnit. Para estos tests, vamos a crear una carpeta específica que podemos llamar calculadora/system-tests, por ejemplo. El siguiente es un ejemplo:

calculadora/system-tests <?php /* * To change this license header, choose License Headers in Project Properties. * To change this template file, choose Tools | Templates * and open the template in the editor. */ /** * Description of indexTest * * @author mauri */ class indexTest extends PHPUnit_Framework_TestCase { /** * @var \RemoteWebDriver */ protected $webDriver; public function setUp() { $capabilities = array(\WebDriverCapabilityType::BROWSER_NAME => 'firefox'); $this->webDriver = RemoteWebDriver::create('http://localhost:4444/wd/hub', $capabilities); } public function tearDown() { $this->webDriver->close(); } protected $url = 'http://www.netbeans.org/'; public function testSimple() { $this->webDriver->get($this->url); //checking that page title contains word 'NetBeans' $this->assertContains('NetBeans', $this->webDriver->getTitle()); } }

Este test podemos crearlo manualmente o bien apoyarnos en NetBeans siguiendo el siguiente proceso:

  1. Sobre el archivo sobre el que queremos crear el test (calculadora/index.php por ejemplo) hacemos clic derecho
  2. En el menú contextual elegimos la opción Tools\Create/Update Tests. En ese momento obtendremos una ventana titulada "Create/Update Tests" que nos preguntará tres cosas:
    • Class name: nombre del caso de prueba. Se crea automáticamente en función del nombre de la clase que estamos probando.
    • Location: lugar donde se almacenará el test. La ruta del test será la elegida anteriormente, es decir calculadora/system-tests
    • Framework: en nuestro caso el framework debe ser Selenium.

    Es psobile que en el paso anterior, NetBeans nos vuelve a preguntar por la ruta a la carpeta donde se almacenarán los tests de Selenium. En tal caso, simplemente seleccionamos la carpeta calculadora/system-tests.

El test PHPUnit

Llegados a este punto, ya se habrá creado nuestro caso de prueba. En el método setUp se crea la conexión con el servidor Selenium. Ahora lo que queremos es definir acciones a llevar a cabo sobre nuestra página. Estas acciones (o comandos) son descritas en https://github.com/facebook/php-webdriver/wiki/Example-command-reference

El código que se presenta a continuación está lleno de defectos, y no es un ejemplo de código bien escrito. Téngase en cuenta que en este caso el objetivo es realizar un test con Selenium y no conseguir una página web bien escrita. Por ello se ha reducido la complejidad al mínimo. Si lo que hemos visto hasta ahora en LMSGI ha valido para algo, seguro que podremos hacerlo mucho mejor

Así, por ejemplo supongamos que calculadora/index.php contiene el siguiente código fuente:

calculadora/index.php <!DOCTYPE html> <html> <head> <title>Calculadora</title> </head> <body> <form method="get" action="calcular.php"> <label for="operando1">Operando1:</label><input type="number" id="operando1" name="operando1" value="0"/><br/> <label for="operando2">Operando2:</label><input type="number" id="operando2" name="operando2" value="0"/><br/> <label for="radio_sumar">Sumar</label><input type="radio" id="radio_sumar" name="operacion" value="sumar"/><br/> <label for="radio_restar">Restar</label><input type="radio" id="radio_restar" name="operacion" value="restar"/><br/> <label for="radio_multiplicar">Mutliplicar</label><input type="radio" id="radio_multiplicar" name="operacion" value="multiplicar"/><br/> <label for="radio_dividir">Dividir</label><input type="radio" id="radio_dividir" name="operacion" value="dividir"/><br/> <input type="submit" id="submit_operacion" value="Calcular"/> </form> </body> </html>

Por otra parte, supongamos que el script calculadora/calcular.php contiene el siguiente código:

Téngase en cuenta que esta no es (ni mucho menos) la mejor forma de obtener información procedente de un formulario. Para mayor referencia, consultar en http://php.net/manual/es/function.filter-input.php

calculadora/calcular.php <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title></title> </head> <body> <?php require "vendor/autoload.php"; $operando1 = $_GET['operando1']; $operando2 = $_GET['operando2']; $operacion = $_GET['operacion']; use \calculadora\operaciones\OperadorBinario; $operadorBinario = new OperadorBinario(); $resultado=0; if ($operacion == "sumar"){ $resultado = $operadorBinario->operacion($operando1,$operando2,OperadorBinario::SUMA); } else if ($operacion == "restar"){ $resultado = $operadorBinario->operacion($operando1,$operando2,OperadorBinario::RESTA); } else if ($operacion == "multiplicar"){ $resultado = $operadorBinario->operacion($operando1,$operando2,OperadorBinario::MULTIPLICACION); } else if ($operacion == "dividir"){ $resultado = $operadorBinario->operacion($operando1,$operando2,OperadorBinario::DIVISION); } else { $resultado = "No se ha encontrado la operación"; } echo "<p id='resultado'>El resultado de la operación es: ".$resultado."</p>"; ?> </body> </html>

Entonces un posible caso de prueba sería el siguiente:

¡OJO! Observa que se han hecho algunos cambios respecto al test que NetBeans originó inicialmente.

calculadora/system-tests/indexTest.php <?php /* * To change this license header, choose License Headers in Project Properties. * To change this template file, choose Tools | Templates * and open the template in the editor. */ /** * Description of indexTest * * @author mauri */ use Facebook\WebDriver\Remote\WebDriverCapabilityType; use Facebook\WebDriver\Remote\RemoteWebDriver; use Facebook\WebDriver\WebDriverBy; class indexTest extends PHPUnit_Framework_TestCase { /** * @var Facebook\WebDriver\Remote\RemoteWebDriver */ protected $webDriver; public function setUp() { $capabilities = array(WebDriverCapabilityType::BROWSER_NAME => 'firefox'); $this->webDriver = RemoteWebDriver::create('http://localhost:4444/wd/hub', $capabilities); } public function tearDown() { $this->webDriver->close(); } protected $url = 'http://localhost/calculadora/'; /** * @dataProvider providerOperacion */ public function testOperacion($operando1, $operando2, $radioInputOperacion,$resultadoEsperado){ $this->webDriver->get($this->url); // Insertamos los valores en los input $this->webDriver->findElement(WebDriverBy::id("operando1"))->sendKeys($operando1); $this->webDriver->findElement(WebDriverBy::id("operando2"))->sendKeys($operando2); // Seleccionamos el input tipo radio con la operación deseada. $this->webDriver->findElement(WebDriverBy::id($radioInputOperacion))->click(); // Hacemos clic en el botón submit del formulario $this->webDriver->findElement(WebDriverBy::id("submit_operacion"))->click(); /* Tras hacer clic en el botón en envío, el servidor * Selenium espera la respuesta desde el servidor web * para continuar con el resto de comandos */ // Una vez que ha llegado la respuesta obtenemos su valor $resultadoObtenido = $this->webDriver->findElement(WebDriverBy::id("resultado"))->getText(); // Comparamos el valor esperado y el obtenido. $this->assertEquals($resultadoEsperado,$resultadoObtenido); } public function providerOperacion(){ return array( 'Suma de 3 + 12' => array(3,12,"radio_sumar",15), 'Resta de 3 - 10' => array(3,10,"radio_restar",-7), 'Multiplicación de 5 * 4' => array(5,4,"radio_multiplicar",20), 'División de 35 / 7' => array(35,7,"radio_dividir",5) ); } }

Si todo ha ido bien, podremos ver como nuestra aplicación pasa todos los tests:

Los tests Selenium han pasado.

Actividad 5. Crea una prueba de sistema para el proyecto de conversión de unidades, que utilice todos los componentes simultáneamente, y utiliza Selenium, PHPUnit y Webdriver para ello.

Las pruebas deben contener al menos 2 conversiones de cada tipo (de cm a pulgadas y de pulgadas a cm).

La evaluación positiva de este ejercicio implicará la demostración en vivo de la ejecución de las pruebas.

Existen otras consideraciones, como por ejemplo:

  • ¿Qué pasa si quiero hacer las pruebas en otro navegador?
  • ¿Qué pasa si quiero automatizar las pruebas en una máquina sin entorno de escritorio?

Para obtener información sobre estos temas, podemos empezar por aquí: https://www.leaseweb.com/labs/2013/09/testing-your-project-with-phpunit-and-selenium/