Hay muchas herramientas en el ecosistema de PHP que proporcionan pruebas de PHP convenientes. Uno de los más famosos es
PHPUnit , que es casi un sinónimo de prueba en este lenguaje. Sin embargo, no se escribe mucho sobre buenos métodos de prueba. Hay muchas opciones sobre por qué y cuándo escribir pruebas, qué tipo de pruebas, etc. Pero para ser sincero,
no tiene sentido escribir un examen si no puede leerlo más tarde .
Las pruebas son un tipo especial de documentación. Como
escribí sobre TDD en PHP anteriormente , la prueba siempre (o al menos debería) decir claramente cuál es la tarea de un código en particular.
Si una prueba no puede expresar esta idea, entonces la prueba es mala.
He preparado un conjunto de técnicas que ayudarán a los desarrolladores de PHP a escribir pruebas buenas, legibles y útiles.
Comencemos con lo básico
Hay un conjunto de técnicas estándar que muchos siguen sin ninguna pregunta. Mencionaré muchos de ellos e intentaré explicar por qué son necesarios.
1. Las pruebas no deben contener operaciones de entrada-salida
La razón principal : las operaciones de E / S son lentas y poco confiables.
Lento : incluso si tiene el mejor hardware del mundo, la E / S seguirá siendo más lenta que los accesos de memoria. Las pruebas siempre deben funcionar rápido, de lo contrario las personas las realizarán muy raramente.
No confiable : algunos archivos, binarios, sockets, carpetas y registros DNS pueden no estar disponibles en algunas máquinas en las que está probando. Cuanto más confíe en las pruebas de E / S, más vinculadas estarán sus pruebas a la infraestructura.
Qué operaciones se relacionan con E / S:
- Lectura y escritura de archivos.
- Llamadas de red.
- Llamadas a procesos externos (usando
exec
, proc_open
, etc.).
Hay situaciones en las que la presencia de operaciones de entrada-salida le permite escribir pruebas más rápido. Pero tenga cuidado: compruebe que tales operaciones funcionan de la misma manera en sus máquinas para el desarrollo, el ensamblaje y la implementación, de lo contrario puede tener serios problemas.
Aísle las pruebas para que no necesiten operaciones de E / S: a continuación proporcioné una solución arquitectónica que evita que las pruebas realicen operaciones de E / S al compartir la responsabilidad entre las interfaces.
Un ejemplo:
public function getPeople(): array { $rawPeople = file_get_contents( 'people.json' ) ?? '[]'; return json_decode( $rawPeople, true ); }
Cuando comience a probar con este método, se creará un archivo local y, de vez en cuando, se crearán instantáneas:
public function testGetPeopleReturnsPeopleList(): void { $people = $this->peopleService ->getPeople();
Para hacer esto, necesitamos establecer requisitos previos para ejecutar pruebas. A primera vista, todo parece razonable, pero de hecho es terrible.
Omitir una prueba debido al hecho de que no se cumplen los requisitos previos no garantiza la calidad de nuestro software. ¡Esto solo ocultará errores!
Arreglamos la situación : aislamos las operaciones de E / S cambiando la responsabilidad a la interfaz.
Ahora sé que
JsonFilePeopleProvider
usará E / S en cualquier caso.
En lugar de
file_get_contents()
puede usar una capa de abstracción como
el sistema de archivos Flysystem , para lo cual es fácil hacer stubs.
¿Y entonces por qué necesitamos
PeopleService
? Buena pregunta Para esto, se necesitan pruebas: para desafiar la arquitectura y eliminar el código inútil.
2. Las pruebas deben ser conscientes y significativas.
La razón principal : las pruebas son una forma de documentación. Manténgalos claros, concisos y legibles.
Claridad y brevedad : sin desorden, sin mil líneas de trozos, sin secuencias de declaraciones.
Legibilidad : las pruebas deben contar una historia. La estructura "dado, cuándo, entonces" es excelente para esto.
Características de una prueba buena y legible:
- Contiene solo las llamadas necesarias al método de
assert
(preferiblemente uno).
- Él explica muy claramente lo que debería suceder en determinadas condiciones.
- Prueba solo una rama de la ejecución del método.
- Él no hace un trozo para todo el universo por el bien de cualquier declaración.
Es importante tener en cuenta que si su implementación contiene expresiones condicionales, operadores de transición o bucles, todos deberían estar explícitamente cubiertos por las pruebas. Por ejemplo, para que las respuestas tempranas siempre contengan una prueba.
Repito: no es una cuestión de cobertura, sino de documentación.
Aquí hay un ejemplo de una prueba confusa:
public function testCanFly(): void { $noWings = new Person(0); $this->assertEquals( false, $noWings->canFly() ); $singleWing = new Person(1); $this->assertTrue( !$singleWing->canFly() ); $twoWings = new Person(2); $this->assertTrue( $twoWings->canFly() ); }
Adaptemos el formato "dado cuándo, entonces" y veamos qué sucede:
public function testCanFly(): void {
Al igual que la sección "Dado", "cuándo" y "entonces" se pueden transferir a métodos privados. Esto hará que su prueba sea más legible.
assertEquals
desastre sin sentido. La persona que lee esto debe rastrear la declaración para entender lo que significa.
El uso de declaraciones específicas hará que su prueba sea mucho más legible.
assertTrue()
debería recibir una variable booleana, no una expresión como
canFly() !== true
.
En el ejemplo anterior, reemplazamos
assertEquals
entre
false
y
$person->canFly()
con un simple
assertFalse
:
¡Ahora todo está muy claro! Si una persona no tiene alas, ¡no debe poder volar! Leer como un poema
Ahora, la sección "Casos adicionales", que aparece dos veces en nuestro texto, es una clara indicación de que la prueba hace demasiadas declaraciones. El método
testCanFly()
es completamente inútil.
Mejoremos la prueba nuevamente:
public function testCanFlyIsFalsyWhenPersonHasNoWings(): void { $person = $this->givenAPersonHasNoWings(); $this->assertFalse( $person->canFly() ); } public function testCanFlyIsTruthyWhenPersonHasTwoWings(): void { $person = $this->givenAPersonHasTwoWings(); $this->assertTrue( $person->canFly() ); }
Incluso podemos cambiar el nombre del método de prueba para que coincida con el escenario real, por ejemplo, en
testPersonCantFlyWithoutWings
, pero de
testPersonCantFlyWithoutWings
todo me conviene.
3. La prueba no debe depender de otras pruebas.
La razón principal : las pruebas deben ejecutarse y ejecutarse con éxito en cualquier orden.
No veo razones suficientes para crear interconexiones entre pruebas. Recientemente me pidieron que hiciera una prueba de función de inicio de sesión, la daré aquí como un buen ejemplo.
La prueba debe:
- Genere un token JWT para iniciar sesión.
- Ejecute la función de inicio de sesión.
- Aprobar el cambio de estado.
Fue así:
public function testGenerateJWTToken(): void {
Esto es malo por varias razones:
- PHPUnit no puede garantizar este orden de ejecución.
- Las pruebas deben poder ejecutarse de forma independiente.
- Las pruebas paralelas pueden fallar al azar.
La forma más fácil de evitar esto es usar el esquema dado, cuándo y luego. Entonces, las pruebas serán más reflexivas, contarán una historia, demostrando claramente sus dependencias, explicando la función que se está probando.
public function testAmazingFeatureChangesState(): void {
También necesitamos agregar pruebas para autenticación, etc. Esta estructura es tan buena que
Behat se usa por defecto .
4. Implemente siempre las dependencias.
La razón principal : un tono muy malo: para crear un trozo para el estado global. La imposibilidad de crear apéndices para dependencias no permite probar la función.
Consejo útil:
Olvídate de las clases con estado estático y las instancias singleton . Si su clase depende de algo, hágalo para que pueda implementarse.
Aquí hay un triste ejemplo:
class FeatureToggle { public function isActive( Id $feature ): bool { $cookieName = $feature->getCookieName();
¿Cómo puedo probar esta respuesta temprana?
Eso es correcto De ninguna manera
Para probarlo, debemos comprender el comportamiento de la clase
Cookies
y asegurarnos de que podemos reproducir todo el entorno asociado con él, lo que da como resultado ciertas respuestas.
No hagas esto.
La situación se puede corregir si implementa una instancia de
Cookies
como una dependencia. La prueba se verá así:
Lo mismo ocurre con los singletones. Entonces, si desea hacer que un objeto sea único, configure correctamente su inyector de dependencia, en lugar de usar el patrón (anti) singleton. De lo contrario, escribirá métodos que son útiles solo para casos como
reset()
o
setInstance()
. En mi opinión, esto es una locura.
¡Es completamente normal cambiar la arquitectura para facilitar las pruebas! Y crear métodos para facilitar las pruebas no es normal.
5. Nunca pruebe métodos protegidos / privados
La razón principal : afectan la forma en que probamos las funciones al determinar la firma del comportamiento: en esta condición, cuando ingreso A, espero obtener B.
Los métodos privados / protegidos no son parte de las firmas de funciones .
Ni siquiera quiero mostrar una forma de "probar" métodos privados, pero daré una pista: solo puedes hacerlo usando la API de
reflexión .
¡Siempre castígate de alguna manera cuando pienses en usar la reflexión para probar métodos privados! Malo, mal desarrollador!
Por definición, los métodos privados solo se llaman internamente. Es decir, no están disponibles públicamente. Esto significa que solo los métodos públicos de la misma clase pueden llamar a dichos métodos.
Si probó todos sus métodos públicos, también probó todos los métodos privados / protegidos . Si este no es el caso, elimine libremente los métodos privados / protegidos; nadie los usa de todos modos.
Consejos avanzados
Espero que aún no estés aburrido. Aún así, tuve que hablar sobre lo básico. Ahora compartiré mi opinión sobre cómo escribir pruebas y decisiones limpias que afecten mi proceso de desarrollo.
Lo más importante que no olvido al escribir pruebas:
- Estudio.
- Comentarios rápidos
- Documentación
- Refactorización
- Diseño durante las pruebas.
1. Pruebas al principio, no al final
Valores : estudio, retroalimentación rápida, documentación, refactorización, diseño durante las pruebas.
Esta es la base de todo. El aspecto más importante, que incluye todos los valores enumerados. Cuando escribe pruebas por adelantado, esto le ayuda a comprender primero cómo debe estructurarse el esquema "dado, cuándo, entonces". Al hacerlo, primero documenta y, lo que es más importante, recuerda y establece sus requisitos como los aspectos más importantes.
¿Es extraño escuchar sobre escribir pruebas antes de la implementación? E imagine lo extraño que es implementar algo, y al probar para descubrir, todas sus expresiones "dado cuándo, entonces" no tienen sentido.
Además, este enfoque verificará sus expectativas cada dos segundos. Obtiene comentarios lo más rápido posible. No importa cuán grande o pequeña se vea la característica.
Las pruebas ecológicas son un área ideal para la refactorización. La idea principal: sin pruebas, sin refactorización. Refactorizar sin pruebas es simplemente peligroso.
Finalmente, estableciendo la estructura "dado cuándo, entonces", se volverá obvio para usted qué interfaces deben tener sus métodos y cómo deben comportarse. Mantener la prueba limpia también lo obligará a tomar constantemente decisiones arquitectónicas diferentes. Esto lo obligará a crear fábricas, interfaces, interrumpir la herencia, etc. ¡Y sí, las pruebas serán más fáciles!
Si sus pruebas son documentos en vivo que explican cómo funciona la aplicación, es imprescindible que lo dejen claro.
2. Mejor sin pruebas que con malas pruebas
Valores : estudio, documentación, refactorización.
Muchos desarrolladores piensan en las pruebas de esta manera: escribiré una función, conduciré el marco de prueba hasta que las pruebas cubran un cierto número de líneas nuevas y las envíe a la operación.
Me parece que debe prestar más atención a la situación cuando un nuevo desarrollador comienza a trabajar con esta función.
¿Qué le dirán las pruebas a esta persona?Las pruebas a menudo son confusas si los nombres no son lo suficientemente detallados. ¿Qué es más claro:
testCanFly
o
testCanFlyReturnsFalseWhenPersonHasNoWings
?
Si sus pruebas son solo un código desordenado que hace que el marco cubra más líneas, con ejemplos que no tienen sentido, entonces es hora de detenerse y pensar si escribir estas pruebas en absoluto.
Incluso tonterías como asignar
$a
y
$b
variables, o asignar nombres que no están relacionados con un uso específico.
Recuerde : sus pruebas son documentos en vivo que intentan explicar cómo debe comportarse su aplicación.
assertFalse($a->canFly())
no documenta mucho. Y
assertFalse($personWithNoWings->canFly())
ya es bastante.
3. Ejecute pruebas intrusivamente
Valores : estudio, retroalimentación rápida, refactorización.
Antes de comenzar a trabajar en funciones, ejecute las pruebas. Si fallan antes de que empiece a trabajar, lo sabrá
antes de escribir el código, y no tendrá que pasar unos minutos preciosos depurando pruebas rotas que ni siquiera le importaban.
Después de guardar el archivo, ejecute las pruebas. Cuanto antes descubra que algo se ha roto, más rápido lo arreglará y seguirá adelante. Si la interrupción del flujo de trabajo para resolver un problema le parece improductivo, imagine que más adelante tendrá que retroceder muchos pasos si no conoce el problema.
Después de conversar con colegas durante cinco minutos o verificar las notificaciones de Github, ejecute las pruebas. Si se sonrojaron, entonces sabes dónde lo dejaste. Si las pruebas son verdes, puede continuar trabajando.
Después de cualquier refactorización, incluso nombres de variables, ejecute las pruebas.
En serio, ejecuta las malditas pruebas. Tan a menudo como presiona el botón Guardar.
PHPUnit Watcher puede hacer esto por usted e incluso enviar notificaciones.
4. Grandes pruebas: gran responsabilidad
Valores : estudio, refactorización, diseño durante las pruebas.
Idealmente, cada clase debería tener una prueba. Esta prueba debe abarcar todos los métodos públicos de esta clase, así como todas las expresiones condicionales o operadores de transición ...
Puedes tomar algo como esto:
- Una clase = un caso de prueba.
- Un método = una o más pruebas.
- Una rama alternativa (if / switch / try-catch / exception) = una prueba.
Entonces, para este código simple, necesitará cuatro pruebas:
Cuantos más métodos públicos tenga, más pruebas se necesitarán.
A nadie le gusta leer documentación larga. Dado que sus pruebas también son documentos, el tamaño pequeño y la importancia solo aumentarán su calidad y utilidad.
También es una señal importante de que su clase está acumulando responsabilidad y es hora de refactorizarla transfiriendo una serie de funciones a otras clases o rediseñando el sistema.
5. Apoyar un conjunto de pruebas para resolver problemas de regresión
Valores : estudio, documentación, retroalimentación rápida.
Considere la función:
function findById(string $id): object { return fromDb((int) $id); }
Usted piensa que alguien está transmitiendo "10", pero en realidad se está transmitiendo "10 plátanos". Es decir, vienen dos valores, pero uno es superfluo. Tienes un error
¿Qué vas a hacer primero? ¡Escribe una prueba que marcará ese comportamiento como erróneo!
public function testFindByIdAcceptsOnlyNumericIds(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage( 'Only numeric IDs are allowed.' ); findById("10 bananas"); }
Por supuesto, las pruebas no transmiten nada. Pero ahora sabes lo que hay que hacer para que transmitan. Corrija el error, haga las pruebas verdes, implemente la aplicación y sea feliz.
Mantenga esta prueba con usted. Siempre que sea posible, en un conjunto de pruebas diseñadas para resolver problemas de regresión.
Eso es todo! Comentarios rápidos, correcciones de errores, documentación, código resistente a la regresión y felicidad.
Palabra final
Gran parte de lo anterior es solo mi opinión personal, desarrollada durante mi carrera. Esto no significa que el consejo sea verdadero o falso, es solo una opinión.