El desarrollo basado en pruebas y las pruebas unitarias son las últimas formas de garantizar que el código siga funcionando como se espera a pesar de las modificaciones y ajustes importantes. En este artículo, aprenderá cómo realizar pruebas unitarias de su código PHP en las capas de módulo, base de datos e interfaz de usuario (UI).
Son las 3 de la mañana. ¿Cómo sabemos que nuestro código sigue funcionando?
Las aplicaciones web se ejecutan las 24 horas del día, los 7 días de la semana, por lo que la pregunta de si mi programa todavía se está ejecutando me molestará por la noche. Las pruebas unitarias me han ayudado a desarrollar suficiente confianza en mi código como para poder dormir bien.
Las pruebas unitarias son un marco para escribir casos de prueba para su código y ejecutar estas pruebas automáticamente. El desarrollo basado en pruebas es un enfoque de pruebas unitarias basado en la idea de que primero se deben escribir pruebas y verificar que estas pruebas puedan encontrar errores, y solo entonces comenzar a escribir el código que debe pasar estas pruebas. Cuando pasan todas las pruebas, la función que desarrollamos estará completa. El valor de estas pruebas unitarias es que podemos ejecutarlas en cualquier momento: antes de registrar el código, después de un cambio importante o después de la implementación en un sistema en ejecución.
Pruebas unitarias de PHP
Para PHP, el marco de pruebas unitarias es PHPUnit2. Este sistema se puede instalar como un módulo PEAR usando la línea de comando PEAR: % pear install PHPUnit2.
Después de instalar el marco, puede escribir pruebas unitarias creando clases de prueba derivadas de PHPUnit2_Framework_TestCase.
Módulo de pruebas unitarias
He descubierto que el mejor lugar para comenzar las pruebas unitarias es el módulo de lógica empresarial de la aplicación. Estoy usando un ejemplo simple: esta es una función que suma dos números. Para comenzar a probar, primero escribimos el caso de prueba como se muestra a continuación.
Listado 1. TestAdd.php
<?phprequire_once 'Add.php';require_once 'PHPUnit2/Framework/TestCase.php';class TestAdd extiende PHPUnit2_Framework_TestCase{ function test1() { $this->assertTrue( add( 1, 2 ) == 3 ); } function test2() { $this->assertTrue( add( 1, 1 ) == }}?>
Esta clase TestAdd tiene dos métodos, ambos usan el prefijo de prueba. Cada método define una prueba, que puede ser tan simple como el Listado 1 o muy compleja. En este caso, simplemente afirmamos que 1 más 2 es igual a 3 en la primera prueba y 1 más 1 es igual a 2 en la segunda prueba.
El sistema PHPUnit2 define el método afirmarTrue(), que se utiliza para probar si el valor de la condición contenido en el parámetro es verdadero. Luego escribimos el módulo Add.php, que inicialmente produjo resultados incorrectos.
Listado 2. Add.php
<?phpfunction add( $a, $b ) { return 0 }?>
Ahora, al ejecutar las pruebas unitarias, ambas pruebas fallan.
Listado 3. Fallos de prueba
% phpunit TestAdd.phpPHPUnit 2.2.1 por Sebastian Bergmann.FFTiempo: 0.0031270980834961Hubo 2 fallos:1) test1(TestAdd)2) test2(TestAdd)FALLOS!!!Pruebas ejecutadas: 2, Fallos: 2, Errores: 0, Pruebas incompletas: 0.
Ahora sé que ambas pruebas funcionan bien. Por lo tanto, la función add() se puede modificar para que realmente haga lo mismo.
Ahora ambas pruebas pasan.
<?phpfunction add( $a, $b ) { return $a+$b }?>
Listado 4. Prueba aprobada
% phpunit TestAdd.phpPHPUnit 2.2.1 por Sebastian Bergmann...Tiempo: 0.0023679733276367OK (2 pruebas)%
aunque Este ejemplo de desarrollo basado en pruebas es muy simple, pero podemos entender sus ideas. Primero creamos el caso de prueba y teníamos suficiente código para ejecutar la prueba, pero el resultado fue incorrecto. Luego verificamos que la prueba realmente falla y luego implementamos el código real para que la prueba pase.
Encuentro que a medida que implemento el código sigo agregando código hasta que tengo una prueba completa que cubre todas las rutas del código. Al final de este artículo, encontrará algunos consejos sobre qué pruebas escribir y cómo escribirlas.
Prueba de base de datos
Después de la prueba del módulo, se pueden realizar pruebas de acceso a la base de datos. Las pruebas de acceso a bases de datos plantean dos cuestiones interesantes. Primero, debemos restaurar la base de datos a algún punto conocido antes de cada prueba. En segundo lugar, tenga en cuenta que esta recuperación puede dañar la base de datos existente, por lo que debemos realizar pruebas en una base de datos que no sea de producción o tener cuidado de no afectar el contenido de la base de datos existente al escribir casos de prueba.
Las pruebas unitarias de la base de datos comienzan desde la base de datos. Para ilustrar este problema, necesitamos usar el siguiente patrón simple.
Listado 5. Schema.sql
DROP TABLE IF EXISTS autores; CREAR TABLA autores (id MEDIUMINT NOT NULL AUTO_INCREMENT, nombre TEXT NOT NULL, PRIMARY KEY (id))
;
A continuación, puede escribir casos de prueba.
Listado 6. TestAuthors.php
<?phprequire_once 'dblib.php';require_once 'PHPUnit2/Framework/TestCase.php';class TestAuthors extends PHPUnit2_Framework_TestCase{ function test_delete_all() { $this->assertTrue( Authors::delete_all() ); } function test_insert() { $this->assertTrue( Autores::delete_all() ); $this->assertTrue( Autores::insert( 'Jack' ) } function test_insert_and_get() { $this->assertTrue( Autores); ::delete_all() ); $this->assertTrue( Autores::insertar( 'Jack' ) ); $this->assertTrue( Autores::insertar( 'Joe' ) ); ; $this->assertTrue( $found != null ); $this->assertTrue( count( $found ) == }}?>
Este conjunto de pruebas cubre la eliminación de autores de la tabla y la inserción de autores en la tabla. Así como funciones como insertar el autor mientras se verifica que el autor existe. Esta es una prueba acumulativa que encuentro muy útil para encontrar errores. Al observar qué pruebas funcionan y cuáles no, puede descubrir rápidamente qué está fallando y luego comprender mejor las diferencias.
A continuación se muestra la versión del código de acceso a la base de datos PHP dblib.php que originalmente produjo el error.
Listado 7. dblib.php
<?phprequire_once('DB.php'); clase Autores{ función estática pública get_db() { $dsn = 'mysql://root:password@localhost/unitdb'; :Connect( $dsn, array() ); if (PEAR::isError($db)) { die($db->getMessage() } return $db; } public static function insert( $name ) { return false; } public static function get_all() { return null }}?>
La ejecución de pruebas unitarias en el código del Listado 8 mostrará que las tres pruebas fallan:
Listado 8. dblib. php
% phpunit TestAuthors.phpPHPUnit 2.2.1 por Sebastian Bergmann.FFFTiempo: 0.007500171661377Hubo 3 fallas:1) test_delete_all(TestAuthors)2) test_insert(TestAuthors)3) test_insert_and_get(TestAuthors)FALLAS!!! Pruebas ejecutadas: 3, Fallas: 3, Errores: 0, Pruebas incompletas: 0.%
Ahora podemos comenzar a agregar el código para acceder correctamente a la base de datos, método por método, hasta que pasen las 3 pruebas. La versión final del código dblib.php se muestra a continuación.
Listado 9. Complete dblib.php
<?phprequire_once('DB.php');class Authors{ public static function get_db() { $dsn = 'mysql://root:password@localhost/unitdb'; ::Connect( $dsn, array() ); if (PEAR::isError($db)) { die($db->getMessage() } return $db } función estática pública eliminar_todos() { $ db; = Autores::get_db(); $sth = $db->prepare( 'ELIMINAR DE autores' ); $db->execute( $sth } función estática pública insertar( $nombre ) { $db = Autores::get_db(); $sth = $db->prepare( 'INSERTAR EN VALORES de autores (nulo,?)' ); $db->execute( $sth, array( $nombre ) } public; función estática get_all() { $db = Autores::get_db(); $res = $db->query( "SELECCIONAR * DE autores" ); $filas = matriz(); ) ) { $rows []= $row; } return $rows; }}?>
Pruebas HTML
El siguiente paso para probar toda la aplicación PHP es probar la interfaz del lenguaje de marcado de hipertexto (HTML). Para realizar esta prueba, necesitamos una página web como la siguiente.
Listado 10. TestPage.php
<?phprequire_once 'HTTP/Client.php';require_once 'PHPUnit2/Framework/TestCase.php';class TestPage extends PHPUnit2_Framework_TestCase{ function get_page( $url ) { $cliente = new HTTP_Client(); ->get( $url ); $resp = $cliente->currentResponse(); return $resp['cuerpo']; función test_get() { $página = TestPage::get_page( 'http://localhost/unidad /add.php'); $this->assertTrue( strlen( $página ) > 0 ); $this->assertTrue( preg_match( '/<html>/', $página ) == } función test_add( ) { $página = TestPage::get_page( 'http://localhost/unit/add.php?a=10&b=20' ); $this->assertTrue( strlen( $page ) > 0 ); afirmarTrue( preg_match( '/<html>/', $página ) == 1 ); preg_match( '/<span id="result">(.*?)</span>/', $página, $out ); $this->assertTrue( $out[1]=='30' ); }}?>
Esta prueba utiliza el módulo Cliente HTTP proporcionado por PEAR. Lo encuentro un poco más simple que la biblioteca de URL del cliente PHP (CURL) incorporada, pero esta última también se puede usar.
Hay una prueba que verifica la página devuelta y determina si contiene HTML. La segunda prueba solicita la suma de 10 y 20 colocando el valor en la URL solicitada y luego verifica el resultado en la página devuelta.
El código de esta página se muestra a continuación.
Listado 11. TestPage.php
<html><body><form><input type="text" name="a" value="<?php echo($_REQUEST['a']); ?>" /> + <input type="text" name="b" value="<?php echo($_REQUEST['b']); ?>" /> =<span id="resultado"><?php echo($_REQUEST ['a']+$_REQUEST['b']); ?></span><br/>><input type="submit" value="Agregar" /></form></body></html >
Esta página es bastante simple. Los dos campos de entrada muestran los valores actuales proporcionados en la solicitud. El intervalo de resultados muestra la suma de estos dos valores. El marcado marca todas las diferencias: es invisible para el usuario, pero visible para las pruebas unitarias. Por tanto, las pruebas unitarias no necesitan una lógica compleja para encontrar este valor. En cambio, recupera el valor de una etiqueta específica. De esta manera, cuando cambie la interfaz, mientras exista el intervalo, la prueba pasará.
Como antes, escriba primero el caso de prueba y luego cree una versión defectuosa de la página. Probamos si hay fallas y luego modificamos el contenido de la página para que funcione. Los resultados son los siguientes:
Listado 12. Falla en la prueba y luego modifica la página
% phpunit TestPage.phpPHPUnit 2.2.1 por Sebastian Bergmann...Tiempo: 0.25711488723755OK (2 pruebas)%
Ambas pruebas pueden pasar, lo que significa que la prueba código Funciona bien.
Al ejecutar pruebas en este código, todas las pruebas se ejecutan sin problemas, por lo que sabemos que nuestro código funciona correctamente.
Pero probar la interfaz HTML tiene un defecto: JavaScript. El código del cliente del Protocolo de transferencia de hipertexto (HTTP) recupera la página, pero no ejecuta JavaScript. Entonces, si tenemos mucho código en JavaScript, tenemos que crear pruebas unitarias a nivel de agente de usuario. La mejor manera que he encontrado para lograr esta funcionalidad es utilizar la funcionalidad de la capa de automatización integrada en Microsoft® Internet Explorer®. Con los scripts de Microsoft Windows® escritos en PHP, puede usar la interfaz del Modelo de objetos componentes (COM) para controlar Internet Explorer para navegar entre páginas y luego usar los métodos del Modelo de objetos de documento (DOM) para buscar páginas después de realizar acciones específicas del usuario. .
Esta es la única forma que conozco de probar unitariamente el código JavaScript front-end. Admito que no es fácil de escribir y mantener, y estas pruebas se rompen fácilmente cuando se realizan incluso cambios mínimos en la página.
Qué pruebas escribir y cómo escribirlas
Al escribir pruebas, me gusta cubrir los siguientes escenarios:
Todas las pruebas positivas
Este conjunto de pruebas garantiza que todo funcione como esperamos.
Todas las pruebas negativas
utilizan estas pruebas una por una para garantizar que se pruebe cada falla o anomalía.
Prueba de secuencia positiva
Este conjunto de pruebas garantiza que las llamadas en la secuencia correcta funcionen como esperamos.
Prueba de secuencia negativa
Este conjunto de pruebas garantiza que las llamadas fallen cuando no se realizan en el orden correcto.
Pruebas de carga
Cuando corresponda, se puede realizar un pequeño conjunto de pruebas para determinar si el rendimiento de estas pruebas está dentro de nuestras expectativas. Por ejemplo, 2000 llamadas deberían completarse en 2 segundos.
Pruebas de recursos
Estas pruebas garantizan que la interfaz de programación de aplicaciones (API) esté asignando y liberando recursos correctamente; por ejemplo, llamando a la API basada en archivos para abrir, escribir y cerrar varias veces seguidas para garantizar que todavía no haya archivos abiertos.
Pruebas de devolución de llamada
Para las API que tienen métodos de devolución de llamada, estas pruebas garantizan que el código se ejecute normalmente si no se define ninguna función de devolución de llamada. Además, estas pruebas también pueden garantizar que el código aún pueda ejecutarse normalmente cuando se definen funciones de devolución de llamada, pero estas funciones de devolución de llamada funcionan incorrectamente o generan excepciones.
Aquí hay algunas ideas sobre las pruebas unitarias. Tengo algunas sugerencias sobre cómo escribir pruebas unitarias:
No utilice datos aleatorios
.Aunque generar datos aleatorios en una interfaz puede parecer una buena idea, queremos evitar hacerlo porque estos datos pueden resultar muy difíciles de depurar. Si los datos se generan aleatoriamente en cada llamada, puede suceder que ocurra un error en una prueba pero no en otra. Si su prueba requiere datos aleatorios, puede generarlos en un archivo y usarlo cada vez que lo ejecute. Con este enfoque, obtenemos algunos datos "ruidosos", pero aún podemos depurar errores.
Pruebas grupales
Podemos acumular fácilmente miles de pruebas que tardan varias horas en ejecutarse. No hay nada de malo en eso, pero agrupar estas pruebas nos permite ejecutar rápidamente un conjunto de pruebas y verificar si hay problemas importantes, y luego ejecutar el conjunto completo de pruebas por la noche.
Escribir API sólidas y pruebas sólidas
Es importante escribir API y pruebas para que no se rompan fácilmente cuando se agrega nueva funcionalidad o se modifica la funcionalidad existente. No existe una fórmula mágica universal, pero una regla general es que las pruebas que "oscilan" (a veces fallan, a veces tienen éxito, una y otra vez) deben descartarse rápidamente.
Conclusión
Las pruebas unitarias son de gran importancia para los ingenieros. Son la base para el proceso de desarrollo ágil (que pone un gran énfasis en la codificación porque la documentación requiere alguna evidencia de que el código funciona de acuerdo con las especificaciones). Las pruebas unitarias proporcionan esta evidencia. El proceso comienza con pruebas unitarias, que definen la funcionalidad que el código debería implementar pero que actualmente no lo hace. Por lo tanto, inicialmente todas las pruebas fallarán. Luego, cuando el código está casi completo, las pruebas pasan. Cuando pasan todas las pruebas, el código se vuelve muy completo.
Nunca escribí código grande ni modifiqué un bloque de código grande o complejo sin usar pruebas unitarias. Por lo general, escribo pruebas unitarias para el código existente antes de modificarlo, solo para asegurarme de saber qué estoy rompiendo (o no rompiendo) cuando modifico el código. Esto me da mucha confianza en que el código que proporciono a mis clientes se ejecuta correctamente, incluso a las 3 de la madrugada.