Esta es la segunda parte de la traducción del manual de buenas prácticas de Testing en Etsy
Testeando las partes juntas
El código ha sido escrito para el cliente y el servidor, pero solo ha sido testeado de manera separada. Nosotros queremos testar que el sistema actual funciona, por ejemplo que nosotros podemos enviar un trabajo real desde un JobClient a un JobServer real.
Una aproximación común es empezar a testear cada interacción entre cualquiera de las 2 partes del sistema (a veces son llamados ‘test de integración’).
¿Qué estamos testeando exactamente?
Preguntate a ti mismo que ocurre entre un cliente y un servidor que no pueden ser testeados el uno o el otro individualmente. Esto son las cosas de las que no debemos preocuparnos a este nivel, porque setear componentes de la vida real para testearlos al mismo tiempo es más difícil que testearlos uno por uno por separado. Así que contaremos con ese esfuerzo.
Las interacciones que podríamos considerar para testar son:
– Si enviamos un trabajo desde el cliente, el servidor lo recibe.
– Si nosotros enviamos un trabajo desde el cliente cuando el servidor el está caído, el cliente falla como esperamos (##ee o algo)
– Si enviamos 100 trabajos al servidor que solo puede recordar 500 cada vez, para de aceptar trabajos.
No hay ningún truco para estableciendo un test de integración. Levantar un servidor, y levantar un cliente. Entonces tenemos que cliente envía un trabajo, y chequea que el servidor lo recibe.
Podemos usar nuestra imaginación y echarle un vistazo a este código. Tanto si el objetivo es un test de interacción como si es un ocurrencia real, tenemos que el cliente envía un trabajo al servidor.
<!--?php <br ?--> class ClientServerIntegrationTest extends PHPUnit_Framework_TestCase { public function testSendAndReceiveJob() { $server = new HttpJobServer("localhost:8888"); $this->startInBackground($server); // Start the server in a background process. $client = new JobClient(new HttpJobTransmitter("localhost:8888")); $client->sendJob("test_job_type", '{ "param1": "value1" }'); $serverQueue = $server->getJobQueue(); $this->assertEquals("test_job_type", $serverQueue[0]->getJobType()); } }
Hay muchos detalles que hemos obviado en este trozo de código. Por ejemplo, ¿cómo prevenir que el servidor de trabajos desencole un trabajo que acaba de entrar antes de que el assert ocurra? Separando la interacción entre el cliente y el servidor para este test, podríamos querer configurar el servidor de trabajos para que nos los ejecute todos, sino solo los trabajos que no forman parte de este test. Quizás podríamos pasar algún tipo de implementación «no-JobExecutor«. Pero lo importante de aquí tenemos que tener en cuenta es que la estructura del test es la de antes — Establecer la estructura del test, setear las entradas y chequear la salidas.
Ejercicios
- ¿Como podríamos testear qué si nuestro JobServer solo encola 500 trabajos y nuestro cliente le envía 100, entonces el servidor tiene que dejar de aceptar trabajos y el cliente debe fallar de la manera esperada?
- ¿Crees que la llamada $server->getJobQueue() para un test rompe la encapsulación de la clase JobServer? ¿Cómo podríamos cambiar este test o el código bajo el test si nosotros no podemos preguntar al JobServer acerca de su estado interno (por ejemplo el trabajo de los trabajos encolados)?
Testeando todo el sistema
Finalmente, vamos a testar todo el sistema de arriba abajo. El objetivo es que testar todo el sistema de la misma manera que hemos testado una clase individual: ¿Hace lo que promete, y es fácil de usar? Los test de más alto nivel, a partir del punto de entrada del usuario al sistema, son una manera de asegurarse de que todas las decisiones de diseño ha llegado hasta el éxito.
Por ejemplo, si nuestro sistema es una aplicación web, la entrada de usuario es una petición HTTP, la petición se pasa a través de un controlador y recorre los diferentes subsistemas de la lógica del controlador: una base de datos, un sistema de plantillas, un indexador de búsqueda, lo que sea.
Para hacer una distinción, podemos testear el controlador por sí solo, haciendo un test de integración:
<!--?php </p--> /* PurchaseConfirmationPageController.php */ class PurchaseConfirmationPageController extends EtsyController { public function doPurchaseConfirmations(PurchaseObject $purchase) { $this->purchaseDatabase->store($purchase); echo 'Purchase complete!'; $this->jobClient->sendJob(<span style="line-height: 1.5;">"send_purchase_confirmation_email", </span><span style="line-height: 1.5;">"{ purchase_id: "{$purchase->purchaseId}" }");</span> } } /* PurchaseConfirmationPageControllerTest.php */ class PurchaseConfirmationPageControllerStubTest extends PHPUnit_Framework_TestCase { public function testDoPurchaseConfirmations() { $stubJobClient = new NoOpClient(); $stubDatabase = new TestPurchaseDatabase(); $controller = new PurchaseConfirmationPageController( $stubDatabase,$stubJobClient); $controller->doPurchaseConfirmations(new PurchaseObject([1234 => "Plush lobster"])); // The getAllSentJobs() method is specific to the NoOpClient class. $this->assertEquals(1, count($stubJobClient->getAllSentJobs())); } }
Pero que estamos testando exactamente? Si nosotros romperemos el código en partes separadas y nuestro controlador es el que coordina estas partes, entonces esto no es más que chequear «cosas» en el código del controlador.
Por otro lado, nosotros podemos testar nuestro sistema de extremo a extremo como si fuésemos usuarios donde nosotros interactuamos con la aplicación web haciendo peticiones HTTP. La idea aquí es testear nuestro código de la manera más ingenua posible — utilizándolo de la misma manera que si fuésemos usuarios finales.. Así que aquí no hay PHPUnit y no interactuamos de manera directa con el código. Trataremos el código como una caja negra e intentaremos hacer un uso real de nuestras entradas y salidas.
En este caso la entrada es una petición HTTP, y la salida — bueno, depende. En este caso, queremos chequear a) que la petición HTTP devuelve un 200 y parece correcta, y b) que recibimos el email de confirmación de la compra.
Establecer estos recursos en nuestro ecosistema de test es un ejercicio para el lector. Para este test, Habremos tenido que instalar una base de datos real, cargar en ella datos para la compra y con ello querer una confirmación. Hemos instalado apache para ejecutar nuestra aplicación web PHP de manera local. Y hemos instalado un servidor local de emails. (Mira en otras páginas de documentación de Etsy como podemos escribir test para usar una base de datos MySQL real, por ejemplo).
Una vez que hemos llegado al punto de tenerlo todo establecido, podríamos testear de cabo a rabo nuestra aplicación realizando un script que hiciese peticiones curl para testear nuestra aplicación web, con Selenium abriendo un naveador, o con test de PHPUnit que realizan llamadas curl. No importa, siempre y cuando si hacemos lo mismo que hace un usuario e inspeccionamos el comportamiento de la aplicación. Estos tests tienen mucho trabajo de instalación y tardan mucho en ejecutarse, así que consideramos que estos tests complementan a la suite de test en profundidad y que los tests PHPUnit son de un grano más fino. Pero haciendo esto estamos seguros de que nuestro sistema es fácil de testear y además seguro que fácil de usar.
Haciendo balance de los diferentes tipos de tests
En cualquiera de los proyectos en los proyectos en los que trabajemos, nosotros tendremos que usar nuestro juicio para determinar los tipos de test que correctos, como utilizarlos y en que grado. Nos preguntamos a nosotros mismos. «¿qué necesito testar exactamente?» y determinaremos la naturaleza del esfuerzo.
##Mocking
Muchos de nuestros tests utilizan un enfoque alternativo para pasar «de puntillas». Si vemos el test JobServer de arriba, hemos probado que un envío de trabajo falla creando una implementación sencilla de la interfaz y el método run(). El problema es que a menudo el código el código que utilizamos no tiene interfaces, por ello nosotros creamos mocks para poder utilizarlos en los tests.
Por ejemplo, si estamos testando un código que utiliza una clase helper para interactuar con la base de datos, y que la esa clase helper hace llamadas reales a MySQL, entonces el código del helper necesita una instancia de MySQL ejecutandose en segundo plano:
<!--?php </p--> class MyDataProcessor { public function processData(DatabaseHelper $dbHelper) { $rows = $dbHelper->lookUpData(self::DATABASE_KEY); // This runs a real MySQL query. $result = 0; foreach ($rows as $row) { // Process this $row... } return $result; } }
Si DatabaseHelper es una clase concreta, nosotros podemos utilizar mocks en el test para no utilizar la clase real:
<!--?php </p--> class MyDataProcessorTest extends PHPUnit_Framework_TestCase { public function testProcessData() { // Create a mock DatabaseHelper object. $mockDbHelper = $this->getMockBuilder('DatabaseHelper') ->disableOriginalConstructor() ->setMethods(array('lookUpData')) ->getMock(); // Mock out a call to its lookUpData() method. $mockDbHelper->expects($this->once()) ->method('lookUpData') ->with($this->equalTo(MyDataProcessor::DATABASE_KEY)) ->will($this->returnValue(array('Data1', 'Data2', 'Data3'))); $dataProcessorUnderTest = new MyDataProcessor(); $result = $dataProcessorUnderTest->processData($mockDbHelper); $this->assertEquals(7, $result); } }
El «mockear» puede hacer más fácil probar un trozo de código de manera aislada, cuando el código no utiliza interfaces. O en algunos casos, puede ser más sencillo mockear uno o dos métodos en lugar de introducir una nueva abstracción. Pero si utilizamos «mocks» como sustituto de las abstracciones – como una manera de conseguir realizar tests sin revisar el diseño del código- entonces estamos matando el código y el diseño de manera profunda. Al igual que en estos ejemplos, utilizar test para ir viendo el diseño y revisar nuestro código (o el de otra persona) hará que nuestro código sea más fácil de utilizar.
Los mocks se describen en la documentación de PHPUnit
Conclusiones
Hemos visto como afrontar tests con mocks y como utilizar mocks o clases dummy para poder ir testando y diseñando aplicaciones web de manera sencilla