Pruebas
1Presentación
Para evitar regresiones durante el desarrollo, es recomendable escribir pruebas automatizadas. Existen varios frameworks de pruebas en PHP, siendo PHPUnit el más utilizado.
Puedes usar PHPUnit directamente para probar unitariamente tus objetos. Es posible que entonces necesites crear un entorno especial en el que ejecutar estas pruebas.
Temma facilita la escritura de pruebas de integración. Una prueba de integración comprueba que la salida de una acción es correcta, según los parámetros proporcionados como entrada.
2Instalación de PHPUnit
Hay varias formas de instalar PHPUnit (consulta la documentación). Aquí, descargaremos el archivo PHAR, lo copiaremos en el directorio bin/ del proyecto y lo haremos ejecutable:
$ wget https://phar.phpunit.de/phpunit-10.phar -O bin/phpunit
$ chmod +x bin/phpunit
Puedes comprobar que todo ha ido bien escribiendo el siguiente comando:
$ bin/phpunit --version
PHPUnit 10.5.2 by Sebastian Bergmann and contributors.
3Funcionamiento
El objeto \Temma\Web\Test puede usarse para lanzar solicitudes que ejecutarán directamente las acciones de los controladores solicitados, sin pasar por un servidor HTTP. Según el tipo de solicitud, son posibles varios tipos de retorno:
- El flujo de salida generado por la vista. Por defecto, será el feed HTML generado por la vista Smarty, pero también puede ser un flujo JSON, un archivo CSV, un feed RSS…
- Las variables de plantilla definidas por el controlador y los plugins.
- Un objeto de tipo \Temma\Web\Response, que recupera información precisa sobre la ejecución (redirección, código de error HTTP, vista, plantilla).
- El componente de inyección de dependencias (el "loader"), que contiene todos los objetos instanciados por Temma y por el código de la aplicación.
La mayoría de las veces, se recuperan las variables de plantilla para comprobar que el código de la aplicación ha funcionado como se esperaba.
Usar el flujo de salida puede ser útil para verificar la no regresión de elementos clave de una página HTML.
El objeto de respuesta puede ser necesario para verificaciones avanzadas.
Finalmente, el componente de inyección de dependencias ofrece un control completo sobre el resultado de la ejecución de la solicitud.
4Controlador de ejemplo
Imaginemos un controlador utilizado para mostrar una lista de artículos y su contenido (archivo controllers/Article.php):
/** Controlador de artículos. */
class Articles extends \Temma\Web\Controller {
/** Muestra la lista de artículos. */
public function list() {
$this['articles'] = $this->_loader->ArticleDao->getList();
}
/** Muestra el contenido de un artículo. */
public function show(int $articleId) {
$this['article'] = $this->_loader->ArticleDao->get($articleId);
if (!$this['article'])
$this->_redirect('/article/list');
}
}
Para la acción list, tenemos la siguiente plantilla (archivo templates/articles/list.tpl):
<html>
<body>
<h1>Artículos</h1>
<ul>
{foreach $articles as $article}
<li>{$article.title}</li>
{/foreach}
</ul>
</body>
Para la acción show, tenemos la siguiente plantilla (archivo templates/articles/show.tpl):
<html>
<body>
<h1>{$article.title}</h1>
{$article.html|raw}
</body>
5Creación de una prueba
Vamos a crear un objeto que contenga dos pruebas de integración (una prueba por acción del controlador) en el archivo tests/ArticlesTest.php:
<?php
class ArticlesTest extends \PHPUnit\Framework\TestCase {
/** Objeto de gestión de pruebas de Temma. */
private \Temma\Web\Test $_test;
/** Inicialización. */
public function setUp() : void {
$this->_test = new \Temma\Web\Test();
}
/** Prueba de la acción 'list'. */
public function testList() {
$data = $this->_test->execData('/articles/list');
$this->assertIsArray($data['articles'] ?? null);
$this->assertNotEmpty($data['articles']);
}
/** Prueba de la acción 'show'. */
public function testShow() {
$data = $this->_test->execData('/articles/show/1');
$this->assertEquals(1, ($data['article']['id'] ?? null));
}
}
- Línea 3: El nombre del objeto debe tener el sufijo Test, y debe heredar del objeto \PHPUnit\Framework\TestCase.
- Líneas 8 a 10: El método setUp() se llama al inicializar el objeto, antes de ejecutar las pruebas. Aquí, instanciamos el objeto de prueba proporcionado por Temma, que se usará más adelante.
-
Líneas 12 a 16: Prueba de la acción list.
- Línea 13: Lanzamos una consulta a la URL /articles/list, que normalmente debería mostrar la lista de artículos, y recuperamos un array que contiene todas las variables de plantilla.
- Línea 14: Comprueba que la variable de plantilla articles existe y que es un array.
- Línea 15: Comprueba que esta variable no está vacía.
-
Líneas 18 a 21: Prueba de la acción show.
- Línea 19: Lanza una consulta a la URL /articles/show/1, que normalmente debería mostrar el contenido de un artículo, y recupera un array que contiene todas las variables de plantilla.
- Línea 20: Comprueba que la variable de plantilla article existe, que contiene una clave id, y que el valor asociado a esta clave es 1.
6Ejecución de pruebas en línea de comandos
Para ejecutar todas las pruebas que se han escrito en el directorio tests/, simplemente ejecuta el siguiente comando:
$ bin/phpunit --bootstrap tests/autoload.php tests
A cambio, deberías obtener una salida como esta:
PHPUnit 10.5.2 by Sebastian Bergmann and contributors.
Runtime: PHP 8.3.6-0ubuntu0.24.04.1
.. 2 / 2 (100%)
Time: 00:00.003, Memory: 22.57 MB
OK (2 tests, 5 assertions)
También es posible ejecutar una sola prueba específica:
$ bin/phpunit --bootstrap tests/autoload.php tests/ArticlesTest.php
7Llamada a URLs
Hay cuatro métodos disponibles para iniciar la ejecución de pruebas:
- execOutput() devuelve el flujo de salida de la vista.
- execData() devuelve las variables de plantilla o la cadena de redirección.
- execResponse() devuelve un objeto \Temma\Web\Response.
- execLoader() devuelve el componente de inyección de dependencias creado para ejecutar la solicitud.
Todos estos métodos pueden recibir hasta cuatro parámetros:
- string $url: (obligatorio) URL a llamar, que comienza con una barra (/).
- string $httpMethod: (opcional) El método HTTP de la solicitud. Por defecto, es el método GET.
- ?array $data: (opcional) Array asociativo que contiene los parámetros GET o POST a transmitir. El tipo (GET o POST) viene determinado por el parámetro $httpMethod.
- ?array $cookies: (opcional) Array asociativo que contiene las cookies a transmitir.
7.1Recuperar el flujo de salida
El método execOutput() se usa para recuperar el flujo de salida generado por la vista. Este método puede devolver una cadena vacía.
Ejemplo:
$html = $this->_test->execOutput('/articles/show/1');
// comprobación simple
$this->assertStringContainsString('<h1>', $html);
// comprobación con una expresión regular
$this->assertMatchesRegularExpression('/<h1>.+<\/h1>/', $html);
7.2Recuperar datos
Vimos antes el método execData(), que devuelve las variables de plantilla definidas por el controlador y los plugins.
De hecho, este método puede devolver tres tipos de datos diferentes:
- Un array asociativo, que contiene las variables de plantilla.
- Una cadena de caracteres, si se ha definido una redirección. En este caso, se devuelve la URL de redirección.
- El valor null, si la ejecución se ha interrumpido.
Ejemplo:
$data = $this->_test->execData('/articles/show/1');
// no debería ser null
$this->assertNotNull($data, "Procesamiento detenido.");
// si es una redirección, el artículo no existe
$this->assertIsNotString($data, "Artículo desconocido.");
// comprueba el identificador del artículo
$this->assertEquals(1, ($data['article']['id'] ?? null));
7.3Recuperar la respuesta
El método execResponse() recupera el objeto \Temma\Web\Response creado durante la ejecución de la solicitud. Este objeto ofrece los siguientes getters:
- getRedirection(): Devuelve la cadena de redirección, o null si no se ha solicitado ninguna redirección.
- getRedirectionCode(): Devuelve el código de redirección (301 o 302).
- getHttpError(): Devuelve el código de error HTTP, o null.
- getHttpCode(): Devuelve el código HTTP de la respuesta (200 por defecto).
- getView(): Devuelve el nombre de la vista, o null si no está definida.
- getTemplatePrefix(): Devuelve el prefijo que se añade al principio de las rutas de las plantillas, o null.
- getTemplate(): Devuelve la ruta de la plantilla usada, o null.
- getHeaders(): Devuelve la lista de cabeceras HTTP definidas.
- getData(): Devuelve un array asociativo que contiene las variables de plantilla definidas por el controlador y los plugins.
También es posible acceder directamente a una variable de plantilla usando una sintaxis de tipo array.
Ejemplo:
$response = $this->_test->execResponse('/articles/show/1');
// si es una redirección, el artículo no existe
$redir = $response->getRedirection();
$this->assertNull($redir, "Artículo desconocido.");
// si el código HTTP no es 200, hay un error
$code = $response->getHttpCode();
$this->assertEquals(200, $code, "Error de procesamiento.");
// la vista debería ser la vista Smarty
$view = $response->getView();
$this->assertEquals('\Temma\Views\Smarty', $view, "Vista incorrecta.");
// deberíamos recuperar la variable de plantilla que contiene el artículo
$this->assertEquals(1, ($response['article']['id'] ?? null));
7.4Uso del componente de inyección de dependencias
Cada vez que se ejecuta una solicitud, se crea un componente de inyección de dependencias, que contiene los objetos instanciados por Temma, así como los usados por el código de tu aplicación (si has usado el componente).
El método execLoader() devuelve el objeto \Temma\Base\Loader creado durante la ejecución de la solicitud. Este componente contiene al menos los siguientes objetos:
- config: El objeto de configuración (tipo \Temma\Web\Config) generado a partir del archivo de configuración etc/temma.php.
- request: El objeto de solicitud (tipo \Temma\Web\Request) generado a partir de la URL solicitada.
- response: El objeto de respuesta (tipo \Temma\Web\Response), descrito en la documentación del método execResponse() anterior.
- session: El objeto de gestión de sesiones (tipo \Temma\Base\Session), que en particular recupera el identificador de sesión, almacenado como cookie.
7.5Parámetros GET y POST
Para enviar parámetros GET o POST, especifica el método a usar como segundo parámetro, y pasa un array asociativo que contenga los parámetros como tercer parámetro.
Ejemplo:
$html = $this->_test->execOutput('/articles/create', 'POST', [
'title' => "Nuevo artículo",
'html' => "<p>bla, bla, bla</p>",
]);
8Configuración
8.1Definición del archivo de configuración
Por defecto, Temma encuentra la ruta del archivo de configuración etc/temma.php. Esto te permite usar la misma configuración que para tus desarrollos (misma base de datos, mismos plugins, etc.).
Pero a veces puede ser deseable usar un archivo de configuración diferente, por ejemplo para conectarse
a una base de datos de prueba específica, o para activar/desactivar ciertos plugins.
En este caso, se deben proporcionar dos parámetros al crear el objeto \Temma\Web\Test:
- ?string $appPath: Ruta a la raíz del proyecto.
- ?string $jsonConfigPath: Ruta al archivo de configuración.
Ejemplo:
$appPath = '/opt/mi_proyecto';
$jsonConfigPath = "$appPath/etc/temma-test.php";
$this->_test = new \Temma\Web\Test($appPath, $jsonConfigPath);
8.2Definición del componente de inyección de dependencias
Al realizar pruebas, es posible que quieras "simular" (mock) algunos objetos, es decir, forzar el uso de un objeto sustitutivo cuando el código de la aplicación quiera llamar a un objeto determinado. Esto puede ser útil para evitar que se lleven a cabo ciertos procesos, como la conexión a un servicio externo.
En este caso, necesitas preparar un componente de inyección de dependencias al que le habrás proporcionado el objeto sustitutivo, dándole el nombre con el que se llama al objeto inicial. A continuación, el componente debe proporcionarse como parámetro al constructor del objeto \Temma\Web\Test (tercer parámetro, o parámetro nombrado loader).
Ejemplo:
// objeto "ArticleDao" ficticio
class MockArticleDao {
/** Devuelve una lista ficticia de artículos. */
public function getList() {
return [
['id' => 1, 'title' => 'Título 1'],
['id' => 2, 'title' => 'Título 2'],
['id' => 3, 'title' => 'Título 3'],
];
}
/** Devuelve un artículo ficticio. */
public function get(int $id) {
return [
'id' => $id,
'title' => "Título $id",
'html' => "<p>bla, bla, bla</p>",
];
}
}
// creación del objeto loader
$loader = new \Temma\Base\Loader([
'ArticleDao' => new MockArticleDao(),
]);
// realiza una prueba usando este loader
$this->_test = new \Temma\Web\Test(loader: $loader);
Ten en cuenta que es posible sobrescribir el componente de inyección de dependencias en el archivo etc/temma.php, usando la directiva loader (consulta la documentación de configuración).
En este caso, recuerda usar el objeto definido en la configuración, u otro objeto sobrecargado que se comporte de manera similar cuando lo use el código de la aplicación.
9Gestión de usuarios
9.1Sesiones
Las sesiones de usuario son creadas automáticamente por Temma, mediante el registro de una cookie de sesión en el navegador. Para las pruebas automatizadas, si deseas probar una secuencia de páginas que requieren una sesión, necesitarás recuperar el identificador de sesión generado al llamar a la primera página, y luego transmitirlo a las páginas siguientes.
$test = new \Temma\Web\Test();
// llama a la primera página
$loader = $test->execLoader('/page1');
// obtiene el ID de sesión
$sessionId = $loader->session->getSessionId();
// crea el array de cookies
$cookies = ['TemmaSession' => $sessionId];
// llama a las páginas siguientes
$data = $test->execData('/page2', 'GET', null, $cookies);
...
El nombre de la cookie de sesión ("TemmaSession" en el ejemplo anterior) debe ser el mismo que el especificado en la variable sessionName del archivo de configuración etc/temma.php (consulta la documentación de configuración).
9.2Prueba de autenticación de usuarios
Tu sitio web puede tener algunas páginas que solo son accesibles para los usuarios autenticados. Para ello, puedes usar el plugin/controlador Auth y el atributo Auth proporcionados por Temma.
En este caso, primero autentica a un usuario, y luego pasa la cookie de sesión de página en página.
$appPath = '/opt/mi_proyecto';
$jsonConfigPath = "$appPath/etc/temma-test.php";
$test = new \Temma\Web\Test($appPath, $jsonConfigPath);
// autenticación
$loader = $test->execLoader('/auth/authentication', 'POST', [
'email' => $email,
]);
$token = $loader->response['token'];
$sessionId = $loader->session->getSessionId();
// validación del token
$cookies = ['TemmaSession' => $sessionId];
$loader = $test->execLoader("/auth/check/$token", 'GET', null, $cookies);
// prueba una página accesible solo para usuarios autenticados
$html = $test->execOutput("/account", 'GET', null, $cookies);
$this->assertStringContainsString('Mi cuenta', $html);
Para que esta autenticación funcione correctamente, el archivo de configuración debe contener la directiva robotCheckDisabled. También es recomendable desactivar el envío de mensajes de conexión:
<?php
return [
'x-security' => [
'auth' => [
'robotCheckDisabled' => true
]
],
'x-email' => [
'disabled' => true
]
];