Continuando con la primera parte de Pruebas Automatizadas en Laravel, esta vez toca hablar sobre los Unit Test.
Las pruebas unitarias (o unit test) son una herramienta fundamental de nuestros desarrollos y, a día de hoy, muchas empresas lo exigen. Y no es para menos! ya que los unit tests nos permiten detectar rápidamente, y en fase de desarrollo, si nuestro código esta generando bugs y si cumple con la lógica que esperamos.
¡Además, son muy fáciles de hacer!
Si bien es un tema bastante extenso, en esta publicación voy mostrarte lo necesario para que comiences a hacer pruebas unitarias en todas tus aplicaciones de Laravel y pases al siguiente nivel 🚀.
Más allá de TDD
Puede que ya hayas escuchado sobre los Unit Test gracias a otra practica excelente llamada «Desarrollo Guiado por Pruebas» (o TDD por sus siglas en ingles), y puede que te hayas sentido frustrado cuando intentaste hacer TDD.
Y es totalmente entendible, ya que TDD es la practica de hacer primero las pruebas y luego el código de la aplicación (llamado código de producción). Por lo tanto, requiere que tengas conocimientos previos sobre el desarrollo de pruebas automatizadas.
En esta publicación no voy a enseñar como hacer TDD. Prefiero que primero entiendas como hacer pruebas automatizadas y cuando la tengas clara y hayas practicado bastante, veras como por vos mismo comenzaras a hacer TDD sin darte cuenta 😉.
Al fin y al cabo da lo mismo hacer las pruebas antes o después. Lo más importante es que tengas pruebas automatizadas.
¿Qué es una Prueba Unitaria?
Como comente en la primera publicación de esta guía, las pruebas unitarias son tests que atacan a una parte especifica del sistema (una función o método, normalmente).
Se caracterizan por ser pruebas que no tienen una gran cobertura de código por si solas pero son las pruebas que más rápido se ejecutan. Por lo tanto, debemos tener una gran cantidad de este tipo de pruebas.
Otro de los grandes beneficios de los Unit Test es que nos obligan a escribir un código de calidad, por lo tanto, el test unitario nos esta haciendo mejores programadores sin que nos demos cuenta (¡A lo maestro Miyagi!). Ya veraz porque.
Comenzando con los Unit Tests
Comencemos con esto. Vamos a suponer que tenemos el siguiente código perteneciente a un ecommerce que estamos construyendo.
Estamos en la parte del checkout y nuestro ecommerce tiene una promoción que, si compras dos o más productos, o si compras un producto que cuesta más de $3000, entonces se aplica un descuento del 50%.
Como podemos ver, es un controlador muy simple que realiza la regla de negocio que establecimos anteriormente. Pero… ¿estamos seguro de eso?, ¿como hacemos para aplicar tests unitarios?, ¿por donde debemos empezar?.
Que tener en cuenta al hacer unit tests
Lo primero que debes saber es que en los bucles (for, while, etc.) y en las condiciones (if, elseif, etc.) es donde se esconden los bugs. Y lo siguiente que tenemos que tener en cuenta son los parámetros de la función/método y, por último, lo que devuelve dicho función/método.
Recapitulando, para aplicar tests unitarios siempre tenemos que tener en cuenta tres cosas:
- Parámetros de entrada.
- Bucles y condiciones.
- Valores de retorno.
Así que, esto es lo que vamos a atacar… ¡pero no tan rápido! si nos detenemos a ver con más detalle el código anterior, vemos que tiene varios problemas que hacen que no podamos aplicar tests unitarios correctamente.
Cosas a corregir antes de aplicar unit tests
Los problemas que tenemos aquí son varios y se repiten en la mayoría de los proyectos en Laravel que me han tocado trabajar.
Primero, dijimos que los unit tests tienen que ser los más veloces en ejecutarse y eso es porque atacan código que no interactua con la base de datos, o con servicios de email, o notificaciones o cualquier otro servicio de entrada/salida que es lo que ralentiza un script.
Y aquí vemos como se esta obteniendo el carrito por medio de una consulta a la base de datos utilizando el modelo Cart
.
$cart = Cart::findOrFail( $request->input('cart_id') );
El segundo de los problemas es que toda la lógica se encuentra en el controlador y los controladores deben probarse con tests funcionales, como vimos en la primera parte de esta guía.
Por lo tanto, para solucionar estos problemas debemos aislar la lógica que nos interesa testear y esto se hace aplicando una técnica de refactorización muy simple que consiste en extraer el código que nos interesa a una nueva clase. Veamos como hacerlo.
De una forma muy simple, tomamos lo que queríamos y lo mandamos a una nueva clase llamada PromoCalculator
.
Además, pase algunos de los valores a constantes para darle más sentido al código. Pero ese if()
esta un poco feo, así que vamos a agregar algunas mejoras más (estas mejoras no tienen que ver con el unit test, pero no esta de más mantener un código limpio):
Ahí se ve mejor. Lo que hice fue pasar todos los valores a constantes y cada condición del if
las pase a métodos privados para darle mas coherencia, legibilidad y seguir buenas prácticas. De esta forma queda todo encapsulado dentro de nuestra clase.
Ahora que logramos aislar nuestra lógica de negocio, estamos listos para aplicar pruebas unitarias y hacer un código más robusto 🏋️♂️.
Aplicando pruebas unitarias
Laravel nos brinda un comando artisan para crear tests y, para seguir una convención nos conviene respetar la estructuras de carpetas.
Entonces, si la clase PromoCalculator se encuentra en app/Services/PromoCalculator.php, debemos crear el test de la siguiente forma:
$ php artisan make:test app/Services/PromoCalculatorTest
Y como puedes ver, debemos finalizar el nombre con la palabra Test.
El comando anterior nos generará un archivo como el siguiente.
Por defecto, el comando artisan siempre nos genera una clase con el nombre que le dimos, que extiende de TestCase y genera un test como ejemplo, que es el método publico, el cual tiene una aserción que verifica que el parámetro sea verdadero. Y, obviamente, en este ejemplo, el tests no arrojara alertas.
Así que, esta es nuestra clase para los tests donde todos los métodos públicos que escribamos corresponderán a un caso de prueba. Por lo tanto, es conveniente escribir el nombre del método lo más descriptible posible para que cualquier compañero entienda que es lo que se esta probando en dicho test.
Por otro lado, la clase TestCase es la que contiene el framework llamado PhpUnit, el cual nos brinda todo lo necesario (como el método assertTrue) para que podamos probar nuestro código.
Ejecutando los tests
Si estas utilizando PhpStorm podrás ver que aparecen unos botones «play» de color verde, al costado del nombre de la clase y del método.
Si hacemos clic en el «doble botón play» que esta al costa del nombre de la clase, PhpStorm ejecutara todos los tests que hayamos escrito en este archivo. Por otro lado, si hacemos clic en el botón play que esta en el nombre del método, solo ejecutara ese test.
Si no estas utilizando PhpStorm, puedes ejecutar el siguiente comando en la terminal estando dentro de tu proyecto.
$ vendor/bin/phpunit
Lo malo de este comando es que, ejecutándolo así, a secas, ejecutara todos los tests que tengas en tu proyecto. Para ejecutar de a un método o de a una clase, como hicimos con PhpStorm, debemos pasarle la opción –filter, de la siguiente manera:
// Para ejecutar solo un test: $ vendor/bin/phpunit --filter methodName path/to/file.php // Para ejecutar todos los tests de una clase: $ vendor/bin/phpunit --filter className
Al ejecutar el test de prueba, veremos algo así:
Donde, el color verde nos indica que todos los tests y aserciones se cumplieron correctamente. Y también vemos el tiempo que tardo en ejecutarse el tests y la memoria consumida para ello.
Ahora, si modificamos el valor del método assertTrue()
a false
, veremos como se indican los fallos de tests.
Vemos como en PhpStorm nos indica que test fallo marcándolo con una cruz roja y la terminal nos marca con color roja el test que fallo y en que linea.
Empezando a desarrollar pruebas para nuestra clase
Vamos a crear nuestro primer test para probar la lógica de nuestro ecommerce. Voy a poner nuevamente la imagen de la clase PromoCalculator
para que la tengamos a mano.
Y escribamos un primer unit test para probar la primer parte del if, o sea, el método areThereMany()
:
Detallando linea por linea, vemos que el nombre del test comienza con la palabra test
y luego la descripción de lo que se esta probando, escrito en CamelCase.
También, si queremos, podríamos escribir el nombre de nuestros tests utilizando snake_case y anotación:
/** @test */ public function se_verifica_si_tenemos_dos_productos_se_realiza_descuento()
Luego, las pruebas se escriben en 3 bloques «Dado/Cuando/Entonces», salvo excepciones.
En el primer bloque («dado»), escribimos los datos con los que queremos probar el test. En este caso, vamos a probar el «caso feliz» donde enviamos dos productos y nos devuelve el total con el 50% aplicado, o sea 50 pesos.
Luego, en el segundo bloque («cuando») se escribe la ejecución de lo que queremos probar, pasandole los datos que preparamos anteriormente.
Y en el último bloque («entonces») ponemos todas las aserciones que creamos convenientes. Para este caso, con solo saber que el valor devuelto es 50, nos alcanza porque nos asegura que se aplico el descuento tal cual esperábamos.
Ahora bien, ejecutamos el test y vemos como tenemos todo en verde:
¡Pero esto no termina acá! El método areThereMany()
que estamos atacando tiene un mayor e igual en su condición y nosotros solo hemos escrito una prueba para el caso de igualdad. Por lo tanto, todavía nos falta escribir los casos para cuando el valor es mayor y otro para cuando el valor es menor.
Pero esto se hace muy fácil con un simple «Copy & Paste», cambiando el nombre de los métodos y acomodando el «Dado» y el «Entonces», ya estaríamos:
El resto de las pruebas las hice con snake_case y anotaciones para que vean como queda.
Si miran el último test verán que en el assert no puse el 50% del valor total, ya que, cuando hay un único producto que su precio no supera los $3000, entonces no se aplica el descuento.
Pero lo que quiero que vean es como fui jugando con los parámetros de entrada para ejercitar la clase PromoCalculator
y que el resultado sea lo que yo esperaba.
Esto es toda la base de los unit tests.
Hagamos más tests
El método ensureExists()
que tiene excepciones y las excepciones son un caso particular a la hora de escribir los tests en el formato «Dado/Cuando/Entonces», de paso practicamos un poco más.
Como pueden ver, con las excepciones se utiliza el método expectEception()
pero este método debe estar siempre al principio del método y luego la ejecución de lo que queremos probar.
Al ejecutar todos nuestros tests, el resultado es este:
Y listo! Ya tenemos todos nuestras pruebas unitarias, solo faltaría probar el método shouldBeApplyDiscount()
pero eso se los dejo para que practiquen ustedes y cualquier duda, la pueden dejar en los comentarios de aquí abajo.
¡Las pruebas unitarias te aplican la gran Miyagi!
Tal vez no te diste cuenta, pero las pruebas unitarias nos obligaron a separar la lógica que vivía en el controlador hacia una clase exclusiva que se encarga únicamente de esa tarea. Y si ya has leído mi publicación de Principios SOLID, te habrás dado cuento que eso es el Principio de Responsabilidad Única.
Y eso es genial! Ya que ahora nuestro código:
- Se ha convertido en re-utilizable porque podemos utilizar la clase en cualquier otra parte de nuestra aplicación.
- Reducimos código en nuestro controlador.
- Hicimos código mas robusto por los mismos tests.
- Y ganamos legibilidad, ya que nuestro código es más fácil de entender.
¡Todo esto por solo aplicar tests unitarios 🤘!
Conclusión
Vimos los grandes beneficios de aplicar pruebas unitarias por el poco tiempo que nos lleva desarrollarlas y lo mejor de todo, es que siempre van a estar ahí para darnos una red de contención. Obviamente, como todo código, hay que mantenerlas y aplicar buenas practicas como si fuera código de producción. He dejado varias cosas en el tintero por ser un poco más avanzadas (que las veremos en futuros posts), pero con lo que vimos ya puedes empezar a hacer tus primeros tests y sentir por vos mismo los beneficios que traen las pruebas unitarias.
Cualquier duda ya sabes que las puedes dejar en los comentarios, que con gusto las responderé. Espero que les haya gustado y nos leemos en la próxima 😉🤙.
Que post tan chido
Muchas gracias Luis!
La verdad que la explicación es excelente y con ejemplos muy útiles , me ayudó bastante , justo estoy en un proyecto con Laravel y quiero comenzar a implementar test . Muchas gracias por el excelente post
Muchas gracias a vos Marcelo por comentar. Y cualquier duda, te esperamos por aquí o por el grupo de facebook «Hablemos de Laravel». Saludos.
Gracias Matias, no conseguia entender estos conceptos que haz explicado a la perfeccion… mil gracias!! Esperando por aprender sobre esas cosas que avanzadas que dejaste en el tintero… Por ahora, acabas de hacer de alguien un mejor desarrollador. Empezare a aplicar lo aprendido.
Hola Vioscar, como estas? Muchas gracias por tu comentario. Me alegra mucho que te haya servido y próximamente seguiré publicando más sobre este tema. Saludos!
Excelente post amigo me sirvió de mucho ya que estoy arrancando un proyecto desde 0 y quería integrar los test automáticos, revise este y el post anterior de este tema y te consulto recomiendas aplicar los dos tipos de test que explicas en ambos artículos, o solo este ultimo? y para el caso de que tenga rutas protegidas (Laravel Passport) por Middleware Auth y role respectivamente como se hace en ese caso que me recomiendas, de antemano muchas gracias, saludos.
Hola Jose, muchas gracias por tu comentario!
Si, cada tipo de test prueba distintas cosas. Así que, si puedes aplicar los dos tipos, mejor.
Acordate que los test unitarios deben probar lógica que no pega contra base de datos y el otro tipo de test si.
Saludos y cualquier duda la podes dejar acá o en el grupo de facebook Hablemos de Laravel.
Hola, excelente post. Tengo una pregunta nada mas, si la funcionalidad no exisitiera aun, es decir no hubieramos codificado la funcionalidad del checkout, deberiamos empezar codificando una primera version de la funcionalidad y tratar de hacerle unit tests a esa primera version (con las refactorizaciones necesarias) o empezamos con el unit test e ir agregando la funcionalidad conforme vaya nuestro unit test como marcan los puristas de TDD?
Me alegro que te gusto el post Carlos!
Si recién estas aprendiendo a hacer tests, no te recomendaría que encares una nueva funcionalidad con TDD. Porque TDD es un cambio de cabeza de como encaras los desarrollos y puede ser medio frustrante si no tenes experiencia haciendo tests.
Hacer primero la funcionalidad y después el tests no esta mal, pero tampoco es lo ideal. Pero te da experiencia desarrollando tests.
En resumen: si la tenes clara haciendo tests, hace la funcionalidad con TDD. Si no, desarrolla la funcionalidad (con código bien feo), aplicale tests y después mejora ese código refactorizando.
Muchas gracias por tu respuesta y tu tiempo. Si, apenas ahora ando tratando de profesionalizar mi desarrollo y estoy tratando de aplicar TDD y si es frustrante al principio, sobre todo cuando no estas muy seguro por donde empezar. Pero tanto este post como el otro que hiciste me han ayudado mucho y abierto un panorama un poco mas claro. Lo que estoy tratando de hacer es de no encuadrarme 100% en como se debe de hacer, si no como dices, tomarle el feeling primero. Solo tenia duda si no me estaba engañando a mi mismo y deberia de seguir a raja tabla la metodologia. Por cierto, mucho gusto, soy de Mexico, saludos hasta ahi a Argentina (estoy asumiendo que ahi te encuentras). Ya me excedí en el mensaje jeje. Abusando de tu amabilidad, una pregunta, un sistema de descuentos dinamico (es decir, el usuario de la aplicacion define los descuentos, los periodos, etc) como lo atacarias? Con un strategy pattern o con un decorator pattern? O con ninguno de los dos. Tengo dificultad en mi cabeza como atacar este problema.
Saludos y gracias.
No es ninguna molestia Carlos, al contrario, me gusta hablar con la gente de la comunidad.
Por qué pensaste atacarlo con decorator o strategy? Te lo pregunto más que nada para entender la lógica de tu negocio. De movida te diría que el decorator no, pero capaz que estás queriendo resolver una lógica y el decorator encaja bien.
Mas que nada porque segun mi entendimiento el decorator es para darle nuevas responsabilidades a una clase pero en tiempo de ejecucion. Entonces, como mi esquema de descuentos es dinamico, cuando el usuario cree un nuevo tipo de descuento, la aplicacion de alguna forma sin tocar el codigo aplique este descuento a los productos que se configuren con este descuento. Aclaro que aun estoy en la fase de tormenta de ideas de como implementar esta funcionalidad. En pseudocodigo mas o menos tendria pensado algo asi:
Product es mi modelo con su unit price, Orden y OrdenDetails tiene estos productos y tendria una clase que recibe la orden y calcula su monto total en base a la qty y el up de cada producto, pero antes verificaria si este producto tiene definido un descuento por lo que tendria un decorator que tome el producto y le aplique los descuentos definidos en la base de datos por el usuario de la aplicacion. Como no se de antemano que descuentos estos serian, veo aun mas complicado poder aplicarle un strategy porque como definiria en tiempo de ejecucion que estrategia generar y aplicar. No se si estoy siendo claro.
Saludos.
Claro, tiene sentido! Y una consulta, ¿el usuario va a poder elegir entre unos descuentos ya pre-existentes (por ejemplo, 2×1, 3×2, 20% de descuento, etc.) o el usuario va a poder customizar todo?
Probablemente tenga pre-cargados los mas comunes, 50% de descuento, 25% de descuento, los que comentas de 2 x 1, no costo de shipping. Pero el cliente dejo en claro que le gustaria poder crear sus propios descuentos asi como tambien manejarlos por periodos, como por ejemplo en nuestro Buen Fin (un fin de semana de 4 dias que viene siendo como una copia barata del Black Friday de los gringos), etc.
Bieen, lindo laburo vas a tener! Vas a tener que aplicar más cosas a demás del decorator, capaz que un abstract factory 🤔.
Pero bueno, anda de a poco y entregandole valor de apoco al cliente para que no joda jejej (MVP) y cualquier duda ya sabes que podes pasar por acá o por el grupo de facebook «Hablemos de Laravel».
Jajaja, si, así sera, pero igual quiero estar preparado para cualquier cosa. No habia pensado en lo del Abstract Factory, pero ahora que lo mencionas, esta dando vueltas mi cabeza en como ya podria aplicarlo jeje. Claro que si, aqui me tendras mucho tiempo moliendo gente jeje. Ya me eche casi todos los articulos que has subido, al jefe de mi trabajo regular no le va a gustar la procrastinacion jejeje, sobre todo porque aqui no usamos php ni laravel.
Saludos y que estes bien.
Gracias por tomarte el tiempo y compartir con nosotros. Muy buen post
Espero los próximos artículos 🙂
Amigo la ultima prueba me genera ese problema: 1) Tests\Feature\app\Services\PromoCalculatorTest::Se_Verifica_Excepcion_Cuando_No_Hay__Productos
PHPUnit\Framework\Exception: Class Tests\Feature\app\Services\EmptyProductsException does not exist, como lo puedo solucionar?
Tienes un error en como importantes la excepción. Fijate que el error comienza con el namespace Test:
Tests\Feature\app\Services\EmptyProductsException
.Arregla el
use
en el test o pone una contrabarra cuando vayas a usar el tests, por ejemplo$this->expectException(\EmptyProductsException);
. Yo te recomiendo que arregles el use. Saludos.Estoy aprendiendo también Laravel y me ha gustado mucho tu post. Gracias!
Me surge una duda en la estructura. Siempre he leído que en los controladores solo se deben poner las funciones ‘index, create, store, show, edit, update and destroy’
¿Como de correcto es meter la función calculatePromo() en un controlador?
¿Sería mejor meterla en un Trait?
Un saludo!
Hola Pablo, como estás? Muchas gracias por tu comentario.
Con respecto a que los controladores deben tener solo los métodos de un CRUD (index, show, create, update, etc.) no es correcto. Porque, no siempre un controlador pertenece a un sistema tipo CRUD. Hay aplicaciones mucho más complejas que conviene poner un nombre más descriptivo, como
calculatePromo
por ejemplo.Hasta pueden existir sistemas CRUD complejos donde hay un controlador por cada método. Por ejemplo, CreateProductController o UpdateProductController, etc. Esto último se hace para tener controladores más fácil de mantener, testear y depurar.
Gracias Matias por responder.
Tendré muy en cuenta tus consejos para mis próximos desarrollos en Laravel 🙂
Buenisimo este y el anterior post, la explicación es muy clara, gracias por compartir.
No tenia idea como empezar con los test, muchas gracias!
El comando: `php artisan make:test app/Services/PromoCalculatorTest`
en Laravel 7, genera la clase en «tests/Features/» (directorio por defecto).
Entonces el namespace de la clase será: `Tests\Feature\app\Services`.
Para crear la clase bajo el directorio Unit se debe añadir el flag: –unit
`php artisan make:test app/Services/PromoCalculatorTest –unit`
Y ahora sí, el namespace de la clase PromoCalculatorTest es: `Tests\Unit\app\Services`
Tenes razón, gracias Moises.
Muchas gracias por el articulo, la verdad q esta web es invaluable.
tengo una pregunta vi que escribiste EmptyProductsExeption esta excepcion la creaste vos de manera personalizada? o es de esas cosas que entrega laravel del estilo PalabraMODELOPalabra devolviendo un resultado? no se si me explico.
Hola Javier, como estas? Muchas gracias por tu comentario!
Si, es una clase que cree yo. El código sería
class EmptyProductsException extends Exception
y lo haga para que el código se entienda más a la hora de leerlo.Excelente post! Me hicieron notar la importancia y sencilles de aplicar la automatización de tests en mi desarrollo.
Saludos,
Gracias a vos por tu comentario!
Excelente, fabuloso, estupendo, explicaciones simples para cosas avanzadas, FELICITACIONES !!!
Muchas gracias por tu comentario Javier.
Excelente post. Muy bien explicado.
Muchas gracias Rafael.
Interesante. recomiendas entonces primero si o si hacer primero los test luego escribir código en el controlador ? ,
o van de la mano
Lo ideal es aplicar una práctica que se llama TDD (Test Driven Design) que dice que primero escribas un pequeño test, luego escribas código para que pase ese test, después refactorizas el código escrito y volves a repetir. Esta técnica es muy buena si previamente conoces todos los casos de pruebas que tenes que hacer para que funcione correctamente tu aplicación.
Te recomiendo mucho que leas sobre esta práctica porque te lleva al siguiente nivel como desarrollador.
Excelente, muchas gracias por tan completo articulo
Excelente post, la verdad siempre me costo aplicar unit tests en laravel pero la sugerencia del refactoreo para procesar la logica en otro lugar no solamente tiene mucho sentido sino que permite utilizar los unit tests en otros lugares.
Ahora, tengo una pequeña pregunta. Supon que estas trabajando con un sistema heredado y debes implementar tests a una clase que tiene mucha logica encima.
Entre la logica ademas hay llamadas a base de datos y por una razon que no discutiremos ahora no es factible realizar un refactor para separar bien las responsabilidades.
Al integrar la base de datos al test dejaria de ser unitario si mal no entiendo. ¿Que puedes sugerir para evitar esto?¿Podria utilizar por ejemplo un mock de una respuesta esperada?
La clase por lo menos separa las consultas de base de datos a otras funciones, podria reemplazar el resultado de esta llamada con php unit? No es necesario entrar en detalle en este tema, pero me serviria si me puedes compartir algun articulo que me sea util
Me gustaría que alguien me explique como hacer pruebas automatizadas en Laravel con un software