<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="es">
		<id>https://1984.lsi.us.es/wiki-egc/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Jmorenol</id>
		<title>Wiki de EGC - Contribuciones del usuario [es]</title>
		<link rel="self" type="application/atom+xml" href="https://1984.lsi.us.es/wiki-egc/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Jmorenol"/>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php/Especial:Contribuciones/Jmorenol"/>
		<updated>2026-04-08T23:12:22Z</updated>
		<subtitle>Contribuciones del usuario</subtitle>
		<generator>MediaWiki 1.29.0</generator>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Archivo:EGC_2025-26_Plantilla_Seguimiento_2.pptx&amp;diff=10243</id>
		<title>Archivo:EGC 2025-26 Plantilla Seguimiento 2.pptx</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Archivo:EGC_2025-26_Plantilla_Seguimiento_2.pptx&amp;diff=10243"/>
				<updated>2025-12-01T19:27:01Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: Jmorenol subió una nueva versión de Archivo:EGC 2025-26 Plantilla Seguimiento 2.pptx&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Pr%C3%A1cticas_-_25/26&amp;diff=10242</id>
		<title>Prácticas - 25/26</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Pr%C3%A1cticas_-_25/26&amp;diff=10242"/>
				<updated>2025-12-01T19:21:57Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[Página_Principal]] -&amp;gt; [[2025/2026]] -&amp;gt; [[Prácticas - 25/26]]&lt;br /&gt;
&lt;br /&gt;
== Prácticas ==&lt;br /&gt;
&lt;br /&gt;
* Práctica 1: '''Instalación del sistema base''' [[Archivo:EGC 2025-26 P1.pdf]]&lt;br /&gt;
* Práctica 2: '''Integración y despliegue continuos ''' [[Archivo:EGC 2025-26 P2.pdf]]&lt;br /&gt;
** [[Workflows solución P2 2526 |Workflows solución para los ejercicios propuestos]]&lt;br /&gt;
* Práctica 3: '''Gestión del código fuente''' [[Archivo:EGC_2025-26_P3.pdf]]&lt;br /&gt;
** Solución ejercicios alumno/a A: [[Archivo:P3 usuario A 2526.pdf]]&lt;br /&gt;
** Solución ejercicios alumno/a B: [[Archivo:P3 usuario B 2526.pdf]]&lt;br /&gt;
* Práctica 4: '''Automatización de pruebas''' [[Archivo:EGC_2025-26_P4.pdf]]&lt;br /&gt;
* Práctica 5: '''Contenedores, dev-containers y aislamiento''' [[Archivo:EGC_2025-26_P5.pdf]]&lt;br /&gt;
*Práctica 6: '''Máquinas virtuales y aprovisionamiento''' [[Archivo:EGC 2025-26 P6.pdf ]]&lt;br /&gt;
** Campo de entrenamiento de Vagrant: [[Tutorial Campo de entrenamiento de Vagrant]]&lt;br /&gt;
** Tutorial configurando una máquina virtual para una aplicación: [[Tutorial configurando vagrant para una aplicación 2526|Tutorial configurando vagrant para una aplicación]]&lt;br /&gt;
** ¡Ahora es el momento de probar con uvlhub! [https://docs.uvlhub.io/installation/installation_with_vagrant]&lt;br /&gt;
* Seguimiento 1: [[Archivo:EGC 2025-26 Plantilla Seguimiento 1.pptx]]&lt;br /&gt;
* Seguimiento 2: [[Archivo:EGC 2025-26 Plantilla Seguimiento 2.pptx]]&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
==== Material adicional para el desarrollo de las sesiones prácticas ====&lt;br /&gt;
&lt;br /&gt;
* Box con la imagen de Ubuntu de los equipos de laboratorio de prácticas : [https://portal.cloud.hashicorp.com/vagrant/discover/cdcetsii/UbuntuEGC_25]&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Archivo:EGC_2025-26_Plantilla_Seguimiento_2.pptx&amp;diff=10241</id>
		<title>Archivo:EGC 2025-26 Plantilla Seguimiento 2.pptx</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Archivo:EGC_2025-26_Plantilla_Seguimiento_2.pptx&amp;diff=10241"/>
				<updated>2025-12-01T19:20:57Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Pr%C3%A1cticas_-_25/26&amp;diff=10235</id>
		<title>Prácticas - 25/26</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Pr%C3%A1cticas_-_25/26&amp;diff=10235"/>
				<updated>2025-11-25T07:20:06Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Prácticas */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[Página_Principal]] -&amp;gt; [[2025/2026]] -&amp;gt; [[Prácticas - 25/26]]&lt;br /&gt;
&lt;br /&gt;
== Prácticas ==&lt;br /&gt;
&lt;br /&gt;
* Práctica 1: '''Instalación del sistema base''' [[Archivo:EGC 2025-26 P1.pdf]]&lt;br /&gt;
* Práctica 2: '''Integración y despliegue continuos ''' [[Archivo:EGC 2025-26 P2.pdf]]&lt;br /&gt;
** [[Workflows solución P2 2526 |Workflows solución para los ejercicios propuestos]]&lt;br /&gt;
* Práctica 3: '''Gestión del código fuente''' [[Archivo:EGC_2025-26_P3.pdf]]&lt;br /&gt;
** Solución ejercicios alumno/a A: [[Archivo:P3 usuario A 2526.pdf]]&lt;br /&gt;
** Solución ejercicios alumno/a B: [[Archivo:P3 usuario B 2526.pdf]]&lt;br /&gt;
* Práctica 4: '''Automatización de pruebas''' [[Archivo:EGC_2025-26_P4.pdf]]&lt;br /&gt;
* Práctica 5: '''Contenedores, dev-containers y aislamiento''' [[Archivo:EGC_2025-26_P5.pdf]]&lt;br /&gt;
*Práctica 6: '''Máquinas virtuales y aprovisionamiento''' [[Archivo:EGC 2025-26 P6.pdf ]]&lt;br /&gt;
** Campo de entrenamiento de Vagrant: [[Tutorial Campo de entrenamiento de Vagrant]]&lt;br /&gt;
** Tutorial configurando una máquina virtual para una aplicación: [[Tutorial configurando vagrant para una aplicación 2526|Tutorial configurando vagrant para una aplicación]]&lt;br /&gt;
** ¡Ahora es el momento de probar con uvlhub! [https://docs.uvlhub.io/installation/installation_with_vagrant]&lt;br /&gt;
* Seguimiento 1: [[Archivo:EGC 2025-26 Plantilla Seguimiento 1.pptx]]&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
==== Material adicional para el desarrollo de las sesiones prácticas ====&lt;br /&gt;
&lt;br /&gt;
* Box con la imagen de Ubuntu de los equipos de laboratorio de prácticas : [https://portal.cloud.hashicorp.com/vagrant/discover/cdcetsii/UbuntuEGC_25]&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Pr%C3%A1cticas_-_25/26&amp;diff=10234</id>
		<title>Prácticas - 25/26</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Pr%C3%A1cticas_-_25/26&amp;diff=10234"/>
				<updated>2025-11-25T07:19:46Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Prácticas */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[Página_Principal]] -&amp;gt; [[2025/2026]] -&amp;gt; [[Prácticas - 25/26]]&lt;br /&gt;
&lt;br /&gt;
== Prácticas ==&lt;br /&gt;
&lt;br /&gt;
* Práctica 1: '''Instalación del sistema base''' [[Archivo:EGC 2025-26 P1.pdf]]&lt;br /&gt;
* Práctica 2: '''Integración y despliegue continuos ''' [[Archivo:EGC 2025-26 P2.pdf]]&lt;br /&gt;
** [[Workflows solución P2 2526 |Workflows solución para los ejercicios propuestos]]&lt;br /&gt;
* Práctica 3: '''Gestión del código fuente''' [[Archivo:EGC_2025-26_P3.pdf]]&lt;br /&gt;
** Solución ejercicios alumno/a A: [[Archivo:P3 usuario A 2526.pdf]]&lt;br /&gt;
** Solución ejercicios alumno/a B: [[Archivo:P3 usuario B 2526.pdf]]&lt;br /&gt;
* Práctica 4: '''Automatización de pruebas''' [[Archivo:EGC_2025-26_P4.pdf]]&lt;br /&gt;
* Práctica 5: '''Contenedores, dev-containers y aislamiento''' [[Archivo:EGC_2025-26_P5.pdf]]&lt;br /&gt;
*Práctica 6: '''Máquinas virtuales y aprovisionamiento''' [[Archivo:EGC 2025-26 P6.pdf ]]&lt;br /&gt;
** Campo de entrenamiento de Vagrant: [[Tutorial Campo de entrenamiento de Vagrant]]&lt;br /&gt;
** Tutorial configurando una máquina virtual para una aplicación: [[Tutorial configurando vagrant para una aplicación 2526|Tutorial configurando vagrant para una aplicación]]&lt;br /&gt;
** ¡Ahora es el momento de probar con uvlhub! [https://docs.uvlhub.io/installation/installation_with_vagrant]&lt;br /&gt;
* Seguimiento 1: [[EGC 2025-26 Plantilla Seguimiento 1.pptx]]&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
==== Material adicional para el desarrollo de las sesiones prácticas ====&lt;br /&gt;
&lt;br /&gt;
* Box con la imagen de Ubuntu de los equipos de laboratorio de prácticas : [https://portal.cloud.hashicorp.com/vagrant/discover/cdcetsii/UbuntuEGC_25]&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Archivo:EGC_2025-26_Plantilla_Seguimiento_1.pptx&amp;diff=10233</id>
		<title>Archivo:EGC 2025-26 Plantilla Seguimiento 1.pptx</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Archivo:EGC_2025-26_Plantilla_Seguimiento_1.pptx&amp;diff=10233"/>
				<updated>2025-11-25T07:19:00Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Pr%C3%A1cticas_-_25/26&amp;diff=10232</id>
		<title>Prácticas - 25/26</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Pr%C3%A1cticas_-_25/26&amp;diff=10232"/>
				<updated>2025-11-25T07:13:52Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Prácticas */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[Página_Principal]] -&amp;gt; [[2025/2026]] -&amp;gt; [[Prácticas - 25/26]]&lt;br /&gt;
&lt;br /&gt;
== Prácticas ==&lt;br /&gt;
&lt;br /&gt;
* Práctica 1: '''Instalación del sistema base''' [[Archivo:EGC 2025-26 P1.pdf]]&lt;br /&gt;
* Práctica 2: '''Integración y despliegue continuos ''' [[Archivo:EGC 2025-26 P2.pdf]]&lt;br /&gt;
** [[Workflows solución P2 2526 |Workflows solución para los ejercicios propuestos]]&lt;br /&gt;
* Práctica 3: '''Gestión del código fuente''' [[Archivo:EGC_2025-26_P3.pdf]]&lt;br /&gt;
** Solución ejercicios alumno/a A: [[Archivo:P3 usuario A 2526.pdf]]&lt;br /&gt;
** Solución ejercicios alumno/a B: [[Archivo:P3 usuario B 2526.pdf]]&lt;br /&gt;
* Práctica 4: '''Automatización de pruebas''' [[Archivo:EGC_2025-26_P4.pdf]]&lt;br /&gt;
* Práctica 5: '''Contenedores, dev-containers y aislamiento''' [[Archivo:EGC_2025-26_P5.pdf]]&lt;br /&gt;
*Práctica 6: '''Máquinas virtuales y aprovisionamiento''' [[Archivo:EGC 2025-26 P6.pdf ]]&lt;br /&gt;
** Campo de entrenamiento de Vagrant: [[Tutorial Campo de entrenamiento de Vagrant]]&lt;br /&gt;
** Tutorial configurando una máquina virtual para una aplicación: [[Tutorial configurando vagrant para una aplicación 2526|Tutorial configurando vagrant para una aplicación]]&lt;br /&gt;
** ¡Ahora es el momento de probar con uvlhub! [https://docs.uvlhub.io/installation/installation_with_vagrant]&lt;br /&gt;
* Seguimiento 1: [https://hdvirtual.us.es/discovirt/index.php/s/4TB88sdzSHBzK7q Gestión del código fuente e incidencias]&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
==== Material adicional para el desarrollo de las sesiones prácticas ====&lt;br /&gt;
&lt;br /&gt;
* Box con la imagen de Ubuntu de los equipos de laboratorio de prácticas : [https://portal.cloud.hashicorp.com/vagrant/discover/cdcetsii/UbuntuEGC_25]&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_configurando_vagrant_para_una_aplicaci%C3%B3n_2526&amp;diff=10223</id>
		<title>Tutorial configurando vagrant para una aplicación 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_configurando_vagrant_para_una_aplicaci%C3%B3n_2526&amp;diff=10223"/>
				<updated>2025-11-18T08:47:01Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Estructura del Proyecto */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
= Despliegue de una Aplicación Python en Vagrant =&lt;br /&gt;
&lt;br /&gt;
== Introducción ==&lt;br /&gt;
Este tutorial te guiará en el despliegue de una aplicación Flask utilizando Vagrant, Ansible y Shell scripts. La finalidad es entender cómo automatizar la configuración y despliegue de aplicaciones en entornos virtuales reproducibles, como los que proporciona Vagrant. &lt;br /&gt;
&lt;br /&gt;
== ¿Por qué utilizamos Vagrant? ==&lt;br /&gt;
Vagrant es una herramienta de administración de entornos virtuales que permite definir en un archivo (llamado `Vagrantfile`) las especificaciones necesarias para crear, configurar y aprovisionar una máquina virtual de manera rápida y consistente. Vagrant es especialmente útil en desarrollo y pruebas, ya que nos permite configurar un entorno de desarrollo idéntico al entorno de producción en cuestión de minutos y garantiza que cada miembro del equipo de desarrollo trabaje en un entorno coherente.&lt;br /&gt;
&lt;br /&gt;
Con Vagrant, evitamos el clásico problema de &amp;quot;en mi máquina funciona&amp;quot; al tener un entorno idéntico en cada ejecución, independientemente de la máquina en la que se esté corriendo el proyecto. En este tutorial, usaremos Vagrant para crear una máquina virtual de Linux donde desplegaremos nuestra aplicación de Flask.&lt;br /&gt;
&lt;br /&gt;
== ¿Qué es el Aprovisionamiento? ==&lt;br /&gt;
Aprovisionar, en el contexto de máquinas virtuales y Vagrant, se refiere al proceso de instalar y configurar el software necesario en una máquina virtual para que pueda ejecutar una aplicación. Cuando aprovisionamos una máquina, estamos automatizando la instalación de dependencias, configuraciones de sistema, servicios necesarios (como bases de datos), y otras tareas de configuración que preparan el entorno para ejecutar la aplicación.&lt;br /&gt;
&lt;br /&gt;
=== Métodos de Aprovisionamiento ===&lt;br /&gt;
En este tutorial veremos dos métodos de aprovisionamiento:&lt;br /&gt;
&lt;br /&gt;
1. '''Mediante un script de shell''': Usando un archivo `.sh` (como `provision.sh`), que contiene comandos de terminal para instalar y configurar los componentes necesarios.&lt;br /&gt;
&lt;br /&gt;
2. '''Mediante Ansible''': Usando un archivo de configuración llamado `playbook.yml`, que describe de manera declarativa lo que queremos instalar y configurar en la máquina virtual.&lt;br /&gt;
&lt;br /&gt;
=== ¿Por qué utilizar Ansible para el Aprovisionamiento? ===&lt;br /&gt;
Ansible es una herramienta de automatización de TI que permite gestionar la configuración, el despliegue y la orquestación de sistemas. Comparado con los scripts de shell, Ansible ofrece varias ventajas:&lt;br /&gt;
* '''Declarativo y legible''': Un playbook de Ansible describe el estado deseado del sistema (qué queremos conseguir), en lugar de los pasos para llegar a ese estado (cómo hacerlo), lo cual hace que sea más legible y fácil de mantener.&lt;br /&gt;
* '''Idempotencia''': Ansible ejecuta las tareas de manera idempotente, lo que significa que si ejecutamos el playbook varias veces, el sistema solo aplicará los cambios necesarios, evitando que se realicen operaciones innecesarias.&lt;br /&gt;
* '''Escalabilidad y organización''': Ansible permite crear playbooks para gestionar múltiples servidores de manera centralizada, lo cual es esencial en ambientes de producción donde hay decenas o cientos de servidores.&lt;br /&gt;
* '''Ecosistema y comunidad''': Ansible es una herramienta popular con una gran comunidad de usuarios y módulos preexistentes para una amplia variedad de tareas, lo que facilita extender la funcionalidad y resolver problemas comunes rápidamente.&lt;br /&gt;
&lt;br /&gt;
=== ¿Cuándo es mejor utilizar un script de shell y cuándo Ansible? ===&lt;br /&gt;
Para proyectos pequeños o configuraciones rápidas, un script de shell puede ser suficiente. Sin embargo, para entornos más complejos, donde la repetición, escalabilidad y legibilidad son factores importantes, Ansible es preferible debido a su idempotencia, claridad y modularidad.&lt;br /&gt;
&lt;br /&gt;
En este tutorial, vamos a provisionar un entorno para nuestra aplicación Flask de ambas maneras, para que puedas comparar los métodos y comprender en qué casos uno puede ser más ventajoso que el otro.&lt;br /&gt;
&lt;br /&gt;
== Nuestra Aplicación: Gestión de Tareas ==&lt;br /&gt;
La aplicación que desplegaremos es una sencilla aplicación de gestión de tareas, utilizada previamente en otras prácticas. Esta aplicación está basada en Flask, un framework de desarrollo web en Python, y ahora utilizará una base de datos MariaDB para almacenar información. El propósito de este tutorial es aprender a desplegar la aplicación en un entorno controlado y reproducible usando Vagrant y los métodos de aprovisionamiento mencionados anteriormente.&lt;br /&gt;
&lt;br /&gt;
== Estructura del Proyecto ==&lt;br /&gt;
Para esta práctica, el proyecto debería presentar la siguiente estructura de archivos:&lt;br /&gt;
 &amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
├── .env                                      &lt;br /&gt;
├── config.py               &lt;br /&gt;
├── app/                   &lt;br /&gt;
│   ├── __init__.py&lt;br /&gt;
│   ├── app.py          &lt;br /&gt;
│   ├── models.py          &lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
└──     └── tasks.html&lt;br /&gt;
 &amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y el siguiente contenido:&lt;br /&gt;
&lt;br /&gt;
===== Código &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt;: =====&lt;br /&gt;
&lt;br /&gt;
El archivo .env es un estándar para almacenar configuraciones que varían entre entornos (desarrollo, pruebas, producción) y que no deben ser hardcodeadas o subidas a repositorios de código (como contraseñas). El paquete python-dotenv cargará estas variables en el entorno de la aplicación al inicio.&lt;br /&gt;
&lt;br /&gt;
'''Finalidad:''' Centralizar todas las credenciales de MariaDB y los secretos de Flask, facilitando que tanto el script de aprovisionamiento de Vagrant como la aplicación Flask accedan a ellas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# .env file (Usado en Local y en Vagrant)&lt;br /&gt;
&lt;br /&gt;
# --- 1. Credenciales de la Base de Datos (Para la aplicación Flask) ---&lt;br /&gt;
DATABASE_USER=flask_user&lt;br /&gt;
DATABASE_PASSWORD=flask_password&lt;br /&gt;
DATABASE_HOST=localhost&lt;br /&gt;
DATABASE_DB=tasks_db&lt;br /&gt;
&lt;br /&gt;
# --- 2. Variables de la Aplicación Flask ---&lt;br /&gt;
SECRET_KEY=una-clave-secreta-larga-y-unica&lt;br /&gt;
&lt;br /&gt;
# --- 3. Credenciales de MariaDB Root ---&lt;br /&gt;
MYSQL_ROOT_PASSWORD=su_contraseña_root_segura&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Código &amp;lt;code&amp;gt;config.py&amp;lt;/code&amp;gt;: =====&lt;br /&gt;
&lt;br /&gt;
Este módulo define clases de configuración que heredan de una base (Config) y permiten aplicar ajustes específicos para diferentes entornos (ej. DEBUG = True solo en DevelopmentConfig).&lt;br /&gt;
&lt;br /&gt;
'''Finalidad:''' Construir la cadena de conexión de la base de datos (SQLALCHEMY_DATABASE_URI) a partir de las variables individuales leídas del entorno (os.environ.get(...)), asegurando que la aplicación sepa cómo conectarse a MariaDB.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
import os&lt;br /&gt;
&lt;br /&gt;
# Configuración base (compartida entre entornos)&lt;br /&gt;
class Config:&lt;br /&gt;
	# Lee SECRET_KEY del .env&lt;br /&gt;
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'fallback-clave-secreta' &lt;br /&gt;
    &lt;br /&gt;
    # Lee las variables individuales de la DB&lt;br /&gt;
    DB_USER = os.environ.get('DATABASE_USER')&lt;br /&gt;
    DB_PASS = os.environ.get('DATABASE_PASSWORD')&lt;br /&gt;
    DB_HOST = os.environ.get('DATABASE_HOST')&lt;br /&gt;
    DB_NAME = os.environ.get('DATABASE_DB')&lt;br /&gt;
&lt;br /&gt;
    # Construye la URL de conexión&lt;br /&gt;
    SQLALCHEMY_DATABASE_URI = (&lt;br /&gt;
        f'mysql+pymysql://{DB_USER}:{DB_PASS}@{DB_HOST}/{DB_NAME}' &lt;br /&gt;
        if DB_USER and DB_PASS and DB_HOST and DB_NAME else None&lt;br /&gt;
    )&lt;br /&gt;
    &lt;br /&gt;
    SQLALCHEMY_TRACK_MODIFICATIONS = False&lt;br /&gt;
&lt;br /&gt;
# Configuración específica para desarrollo&lt;br /&gt;
class DevelopmentConfig(Config):&lt;br /&gt;
    DEBUG = True&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Código &amp;lt;code&amp;gt;__init__.py&amp;lt;/code&amp;gt;: =====&lt;br /&gt;
&lt;br /&gt;
Este archivo es el módulo de inicialización del paquete Python app. Contiene una única línea de código que expone la función principal del paquete.&lt;br /&gt;
&lt;br /&gt;
'''Finalidad:''' Actúa como un proxy o puente para que cualquier otro archivo que necesite acceder a la función fábrica de la aplicación (como el comando flask o scripts externos) pueda importarla directamente usando from app import create_app, sin tener que especificar el archivo interno app.app. Simplemente hace que la función create_app definida en app.py sea accesible al nivel superior del paquete app.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Código &amp;lt;code&amp;gt;app.py&amp;lt;/code&amp;gt;: =====&lt;br /&gt;
&lt;br /&gt;
Este módulo contiene la función fábrica de la aplicación (create_app), el corazón de la arquitectura Flask. Es responsable de crear y configurar la instancia de la aplicación de manera flexible, permitiendo diferentes configuraciones (como Desarrollo o Pruebas).&lt;br /&gt;
&lt;br /&gt;
'''Finalidad:''' La función create_app es el punto de control central que:&lt;br /&gt;
&lt;br /&gt;
1) Carga el Entorno: Usa load_dotenv() para cargar variables de configuración (credenciales de DB, SECRET_KEY) desde el archivo .env.&lt;br /&gt;
&lt;br /&gt;
2) Configura la Aplicación: Aplica los ajustes específicos de entorno (tomados de config.py) a la instancia de Flask.&lt;br /&gt;
&lt;br /&gt;
3) Inicializa Extensiones: Vincula extensiones como SQLAlchemy (db.init_app) y Flask-Migrate a la aplicación.&lt;br /&gt;
&lt;br /&gt;
4) Registra Rutas: Agrega los módulos de rutas (los Blueprints) a la aplicación, completando la configuración antes de ser devuelta.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
from .models import db # SQLAlchemy instance&lt;br /&gt;
from flask_migrate import Migrate&lt;br /&gt;
from config import DevelopmentConfig # Importamos la configuración&lt;br /&gt;
from dotenv import load_dotenv&lt;br /&gt;
import os&lt;br /&gt;
&lt;br /&gt;
load_dotenv()&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def create_app(config_class=DevelopmentConfig): # Usa Desarrollo por defecto&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    &lt;br /&gt;
    # 1. Aplicar la configuración&lt;br /&gt;
    app.config.from_object(config_class)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Inicializar extensiones&lt;br /&gt;
    db.init_app(app)&lt;br /&gt;
    # Inicializar Flask-Migrate (necesita app y db)&lt;br /&gt;
    Migrate(app, db) &lt;br /&gt;
&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
        &lt;br /&gt;
    return app&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Código &amp;lt;code&amp;gt;models.py&amp;lt;/code&amp;gt;: =====&lt;br /&gt;
&lt;br /&gt;
Este módulo define el Modelo Relacional de Objetos utilizando Flask-SQLAlchemy (ORM).&lt;br /&gt;
&lt;br /&gt;
'''Finalidad:''' Declarar la estructura de la tabla tasks y proporcionar funciones de acceso a datos de alto nivel (get_all_tasks, create_task) que interactúan con la base de datos de manera segura dentro de un contexto de aplicación (current_app.app_context()).&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
from flask_sqlalchemy import SQLAlchemy&lt;br /&gt;
from flask import current_app&lt;br /&gt;
&lt;br /&gt;
db = SQLAlchemy()&lt;br /&gt;
&lt;br /&gt;
class Task(db.Model):&lt;br /&gt;
    __tablename__ = 'tasks'&lt;br /&gt;
    id = db.Column(db.Integer, primary_key=True)&lt;br /&gt;
    title = db.Column(db.String(100), nullable=False)&lt;br /&gt;
    done = db.Column(db.Boolean, default=False)&lt;br /&gt;
    &lt;br /&gt;
    def to_dict(self):&lt;br /&gt;
        return {&lt;br /&gt;
            'id': self.id,&lt;br /&gt;
            'title': self.title,&lt;br /&gt;
            'done': self.done&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
# --- Funciones de Acceso a Datos ---&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    with current_app.app_context():&lt;br /&gt;
        return [task.to_dict() for task in Task.query.order_by(Task.id).all()]&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    with current_app.app_context():&lt;br /&gt;
        new_task = Task(title=title, done=False)&lt;br /&gt;
        db.session.add(new_task)&lt;br /&gt;
        db.session.commit()&lt;br /&gt;
        return new_task.to_dict()&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===== Código &amp;lt;code&amp;gt;routes.py&amp;lt;/code&amp;gt;: =====&lt;br /&gt;
&lt;br /&gt;
Este módulo utiliza el concepto de Blueprint de Flask para organizar las rutas y vistas de la aplicación, separando la lógica de enrutamiento de la lógica de la fábrica.&lt;br /&gt;
&lt;br /&gt;
'''Finalidad:''' Define los endpoints HTTP (/, /add_task, /tasks). Incluye rutas para la interfaz web (render_template) y endpoints API (jsonify) que llaman a las funciones de acceso a datos definidas en models.py.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===== Archivo &amp;lt;code&amp;gt;templates/tasks.html&amp;lt;/code&amp;gt;: =====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Script &amp;lt;code&amp;gt;local_setup.sh&amp;lt;/code&amp;gt; para lanzar la app en local =====&lt;br /&gt;
&lt;br /&gt;
Si quieres lanzar la aplicación de manera local, puedes utilizar este script:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
&lt;br /&gt;
# =========================================================================&lt;br /&gt;
# SCRIPT DE SETUP Y LANZAMIENTO LOCAL PARA FLASK CON MARIADB&lt;br /&gt;
# =========================================================================&lt;br /&gt;
&lt;br /&gt;
# --- 0. Asegurar la ubicación ---&lt;br /&gt;
# Cambia al directorio del script. Esto garantiza que encuentre .env y .venv.&lt;br /&gt;
cd &amp;quot;$(dirname &amp;quot;$0&amp;quot;)&amp;quot;&lt;br /&gt;
echo &amp;quot;Directorio de trabajo actual: $(pwd)&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# --- 1. Variables y Rutas del Entorno Virtual ---&lt;br /&gt;
# Definimos las rutas a los binarios DENTRO del venv para usar rutas ABSOLUTAS.&lt;br /&gt;
VENV_DIR=&amp;quot;.venv&amp;quot;&lt;br /&gt;
PYTHON_BIN=&amp;quot;$VENV_DIR/bin/python&amp;quot;&lt;br /&gt;
PIP_BIN=&amp;quot;$VENV_DIR/bin/pip&amp;quot;&lt;br /&gt;
FLASK_BIN=&amp;quot;$VENV_DIR/bin/flask&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# --- 2. Cargar Variables de Configuración desde .env ---&lt;br /&gt;
echo &amp;quot;--- 1. Cargando configuración desde el archivo .env ---&amp;quot;&lt;br /&gt;
&lt;br /&gt;
if [ ! -f .env ]; then&lt;br /&gt;
    echo &amp;quot;❌ ERROR: El archivo .env no se encontró en $(pwd). ¡Crealo primero!&amp;quot;&lt;br /&gt;
    exit 1&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
. .env&lt;br /&gt;
&lt;br /&gt;
# Comprobación de carga&lt;br /&gt;
if [ -z &amp;quot;$DATABASE_DB&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;❌ ERROR: Las variables de la base de datos (ej. DATABASE_DB) no se cargaron correctamente.&amp;quot;&lt;br /&gt;
    exit 1&lt;br /&gt;
fi&lt;br /&gt;
echo &amp;quot;✅ Variables cargadas: Usuario=$DATABASE_USER, DB=$DATABASE_DB&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# --- 3. Preparar Entorno Python e Instalar Dependencias ---&lt;br /&gt;
echo &amp;quot;--- 2. Creando Entorno Virtual Python (.venv) ---&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Crear el entorno virtual si no existe&lt;br /&gt;
if [ ! -d &amp;quot;$VENV_DIR&amp;quot; ]; then&lt;br /&gt;
    python3 -m venv &amp;quot;$VENV_DIR&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
# Usando el PIP ABSOLUTO del entorno virtual para la instalación&lt;br /&gt;
echo &amp;quot;--- Instalando dependencias necesarias (Flask, SQLAlchemy, PyMySQL, etc.) ---&amp;quot;&lt;br /&gt;
&amp;quot;$PIP_BIN&amp;quot; install --upgrade pip&lt;br /&gt;
# Lista de dependencias clave para el proyecto:&lt;br /&gt;
&amp;quot;$PIP_BIN&amp;quot; install flask python-dotenv flask-sqlalchemy pymysql flask-migrate&lt;br /&gt;
&lt;br /&gt;
# --- 4. Generar requirements.txt ---&lt;br /&gt;
echo &amp;quot;--- 3. Generando requirements.txt a partir del entorno actual ---&amp;quot;&lt;br /&gt;
&amp;quot;$PIP_BIN&amp;quot; freeze &amp;gt; requirements.txt&lt;br /&gt;
echo &amp;quot;✅ requirements.txt creado con las dependencias instaladas.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# --- 5. Configuración de MariaDB (Usuario y Base de Datos) ---&lt;br /&gt;
echo &amp;quot;--- 4. Configurando usuario y base de datos en MariaDB... ---&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Se ejecuta el comando SQL con las variables cargadas&lt;br /&gt;
# Esto requiere permisos de sudo para acceder al usuario 'root' de MariaDB&lt;br /&gt;
sudo mysql -u root &amp;lt;&amp;lt;EOF&lt;br /&gt;
CREATE DATABASE IF NOT EXISTS $DATABASE_DB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;&lt;br /&gt;
CREATE USER IF NOT EXISTS '$DATABASE_USER'@'localhost' IDENTIFIED BY '$DATABASE_PASSWORD';&lt;br /&gt;
GRANT ALL PRIVILEGES ON $DATABASE_DB.* TO '$DATABASE_USER'@'localhost';&lt;br /&gt;
FLUSH PRIVILEGES;&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;✅ Usuario '$DATABASE_USER' y base de datos '$DATABASE_DB' configurados.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# --- 6. Inicializar y Aplicar Migraciones ---&lt;br /&gt;
echo &amp;quot;--- 5. Aplicando migraciones de base de datos (Flask-Migrate)... ---&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Exporta la variable FLASK_APP&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
&lt;br /&gt;
# Usando el binario de Flask del VENV para ejecutar la migración&lt;br /&gt;
&amp;quot;$FLASK_BIN&amp;quot; db upgrade&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;✅ Migraciones completadas. Las tablas están listas.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# --- 7. Lanzar la Aplicación ---&lt;br /&gt;
echo &amp;quot;--- 6. Lanzando el servidor Flask en http://127.0.0.1:5000 ---&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Usando el binario de Flask del VENV para ejecutar la aplicación&lt;br /&gt;
&amp;quot;$FLASK_BIN&amp;quot; run&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Para lanzarlo, usa:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
chmod +x local_setup.sh&lt;br /&gt;
./local_setup.sh&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
= Parte 1: Aprovisionamiento mediante un Script de Shell =&lt;br /&gt;
En la primera parte del tutorial, aprovisionaremos el entorno de desarrollo con un script de shell (`provision.sh`).&lt;br /&gt;
&lt;br /&gt;
=== ¿Qué es un archivo con extensión .sh? ===&lt;br /&gt;
&lt;br /&gt;
Un archivo con extensión .sh es un '''script de shell''', un archivo de texto que contiene una serie de comandos escritos en un lenguaje de scripting específico para el shell, el intérprete de comandos que interactúa con el sistema operativo. Los scripts .sh se usan ampliamente en sistemas Unix y Linux, aunque también funcionan en otros entornos compatibles con shells como bash, sh o zsh.&lt;br /&gt;
&lt;br /&gt;
==== ¿Para qué sirven los archivos .sh? ====&lt;br /&gt;
&lt;br /&gt;
Los archivos .sh permiten automatizar tareas, ya que el shell ejecuta cada comando en el archivo de forma secuencial, sin necesidad de intervención manual. Esto es especialmente útil para configurar sistemas, instalar paquetes, mover o copiar archivos, configurar redes, iniciar aplicaciones y realizar prácticamente cualquier acción que se pueda ejecutar desde la línea de comandos.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en el contexto de nuestro proyecto con Vagrant y Flask, usaremos un archivo .sh llamado provision.sh para:&lt;br /&gt;
&lt;br /&gt;
Instalar dependencias como Python y Flask.&lt;br /&gt;
Configurar la base de datos.&lt;br /&gt;
Preparar el entorno de la aplicación.&lt;br /&gt;
Esto permite que, al ejecutar vagrant up, el script se ejecute automáticamente, configurando la máquina virtual sin necesidad de realizar los pasos de configuración uno a uno.&lt;br /&gt;
&lt;br /&gt;
==== Cómo se ejecuta un archivo .sh ====&lt;br /&gt;
&lt;br /&gt;
Para ejecutar un archivo .sh en un sistema Linux o Mac, simplemente hay que abrir una terminal y escribir: &amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt; sh nombre_del_archivo.sh &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
O bien, puedes ejecutar el script con permisos adicionales o en otro shell específico, como bash: &amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt; bash nombre_del_archivo.sh &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Cuando Vagrant encuentra un archivo .sh especificado en el Vagrantfile (como en config.vm.provision &amp;quot;shell&amp;quot;, path: &amp;quot;provision.sh&amp;quot;), ejecuta ese archivo automáticamente en la máquina virtual como parte del proceso de aprovisionamiento. Esto garantiza que la máquina esté configurada correctamente cada vez que se inicia.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Crear el Vagrantfile ==&lt;br /&gt;
&lt;br /&gt;
Recuerda que el Vagrantfile es el archivo de configuración que Vagrant utiliza para definir y gestionar una máquina virtual. Este archivo es el núcleo de cualquier proyecto que utilice Vagrant, ya que contiene instrucciones específicas para la configuración de la máquina virtual, incluyendo el sistema operativo, red, sincronización de carpetas, y scripts de aprovisionamiento.&lt;br /&gt;
&lt;br /&gt;
Al colocar el Vagrantfile en el directorio raíz del proyecto (flask_testing_project/), mantenemos todos los recursos y configuraciones necesarias para el despliegue de la aplicación en un solo lugar. Esto hace que el proyecto sea fácil de portar y reproducir, ya que cualquier persona con este directorio y Vagrant puede ejecutar el proyecto en una máquina virtual idéntica.&lt;br /&gt;
&lt;br /&gt;
A continuación, crea un archivo llamado Vagrantfile en el directorio flask_testing_project/ y añade el siguiente contenido:&lt;br /&gt;
&lt;br /&gt;
 &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# Vagrantfile&lt;br /&gt;
# -*- mode: ruby -*-&lt;br /&gt;
# vi: set ft=ruby :&lt;br /&gt;
&lt;br /&gt;
Vagrant.configure(&amp;quot;2&amp;quot;) do |config|&lt;br /&gt;
  &lt;br /&gt;
  # 1. Sistema Operativo&lt;br /&gt;
  config.vm.box = &amp;quot;ubuntu/jammy64&amp;quot; &lt;br /&gt;
  &lt;br /&gt;
  # 2. Mapear puerto de Flask (5000)&lt;br /&gt;
  config.vm.network &amp;quot;forwarded_port&amp;quot;, guest: 5000, host: 5000&lt;br /&gt;
  &lt;br /&gt;
  # 3. Provisionamiento: Ejecuta el script completo&lt;br /&gt;
  config.vm.provision &amp;quot;shell&amp;quot;, path: &amp;quot;provision.sh&amp;quot;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
 &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Explicación línea a línea del Vagrantfile ===&lt;br /&gt;
&lt;br /&gt;
El archivo Vagrantfile define las especificaciones para crear y configurar la máquina virtual (VM) en la que se ejecutará tu aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;Vagrant.configure(&amp;quot;2&amp;quot;) do |config|&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta línea inicia el bloque de configuración del entorno de Vagrant. Indica que estamos utilizando la versión 2 del formato de configuración y asigna un objeto llamado config que se usará para definir todos los ajustes de la máquina virtual.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;config.vm.box = &amp;quot;ubuntu/jammy64&amp;quot;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta es la línea que define el Sistema Operativo de la máquina virtual. &amp;quot;ubuntu/jammy64&amp;quot; es el nombre de la &amp;quot;caja base&amp;quot; (la imagen del sistema operativo) que Vagrant descargará e instalará. En este caso, se especifica una versión de Ubuntu 64 bits (Jammy Jellyfish, o 22.04 LTS).&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;config.vm.network &amp;quot;forwarded_port&amp;quot;, guest: 5000, host: 5000&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta línea se encarga del Mapeo de Puertos. Le indica a Vagrant que debe redirigir el tráfico. Específicamente, el puerto 5000 de la máquina anfitriona (tu máquina local, el host) se reenvía al puerto 5000 dentro de la máquina virtual (guest). Esto es crucial porque permite que accedas a la aplicación Flask (que se ejecuta dentro de la VM en el puerto 5000) desde tu navegador web local visitando http://localhost:5000.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;config.vm.provision &amp;quot;shell&amp;quot;, path: &amp;quot;provision.sh&amp;quot;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta línea define el aprovisionamiento. Le indica a Vagrant que, después de arrancar e instalar el sistema operativo, debe ejecutar un script de automatización. El tipo de provisioner es &amp;quot;shell&amp;quot;, y el argumento path: &amp;quot;provision.sh&amp;quot; especifica que el archivo a ejecutar es provision.sh, el cual debe encontrarse en el mismo directorio que este Vagrantfile. Este script se encargará de instalar MariaDB, Python y configurar la base de datos y la aplicación.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;end&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta línea simplemente cierra el bloque de configuración iniciado por Vagrant.configure(&amp;quot;2&amp;quot;).&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Crear el script de aprovisionamiento ==&lt;br /&gt;
&lt;br /&gt;
Este script, llamado provision.sh, es ejecutado por Vagrant cuando se levanta la máquina virtual (vagrant up). Su objetivo es automatizar la instalación de software, la configuración de la base de datos y el despliegue inicial de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
En la raíz del proyecto, al mismo nivel que el Vagrantfile, crea un script provision.sh con este contenido:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
&lt;br /&gt;
# --- 1. Cargar Variables del .env ---&lt;br /&gt;
# Carga las credenciales de la DB y la aplicación desde el archivo unificado .env&lt;br /&gt;
echo &amp;quot;--- 1. Cargando configuración desde el archivo .env ---&amp;quot;&lt;br /&gt;
set -a&lt;br /&gt;
# Usamos el path /vagrant para acceder a los archivos del directorio compartido&lt;br /&gt;
source /vagrant/.env &lt;br /&gt;
set +a&lt;br /&gt;
&lt;br /&gt;
PROJECT_DIR=&amp;quot;/vagrant&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# --- 2. Instalación de Dependencias del Sistema ---&lt;br /&gt;
echo &amp;quot;--- 2. Instalando MariaDB, Python (&amp;gt;=3.10) y utilidades ---&amp;quot;&lt;br /&gt;
sudo apt-get update&lt;br /&gt;
# python3-dev es necesario para compilar algunas dependencias&lt;br /&gt;
sudo apt-get install -y mariadb-server python3-pip python3-venv git python3-dev &lt;br /&gt;
&lt;br /&gt;
# --- 3. Configuración de MariaDB (Idempotente) ---&lt;br /&gt;
echo &amp;quot;--- 3. Configurando MariaDB y creando usuario '$DATABASE_USER' ---&amp;quot;&lt;br /&gt;
sudo systemctl enable mariadb&lt;br /&gt;
sudo systemctl start mariadb&lt;br /&gt;
sleep 5 &lt;br /&gt;
&lt;br /&gt;
# Los comandos SQL deben usar la sintaxis más compatible para asegurar la creación del usuario.&lt;br /&gt;
sudo mysql -u root &amp;lt;&amp;lt;EOF&lt;br /&gt;
-- 3a. Crear la base de datos&lt;br /&gt;
CREATE DATABASE IF NOT EXISTS $DATABASE_DB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;&lt;br /&gt;
&lt;br /&gt;
-- 3b. Crear/Modificar el usuario con la contraseña&lt;br /&gt;
-- Usamos la sintaxis estándar para compatibilidad.&lt;br /&gt;
CREATE USER IF NOT EXISTS '$DATABASE_USER'@'localhost' IDENTIFIED BY '$DATABASE_PASSWORD';&lt;br /&gt;
&lt;br /&gt;
-- 3c. Asegurar que el plugin de autenticación sea compatible con PyMySQL (si la versión de MariaDB lo requiere)&lt;br /&gt;
-- Nota: Si usas una versión reciente de MariaDB, este paso es implícito o usa IDENTIFIED BY.&lt;br /&gt;
-- Lo mantenemos simple para evitar el ERROR 1064.&lt;br /&gt;
&lt;br /&gt;
-- 3d. Otorgar permisos sobre la base de datos&lt;br /&gt;
GRANT ALL PRIVILEGES ON $DATABASE_DB.* TO '$DATABASE_USER'@'localhost';&lt;br /&gt;
&lt;br /&gt;
-- 3e. Aplicar los cambios&lt;br /&gt;
FLUSH PRIVILEGES;&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;✅ MariaDB configurado.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# --- 4. Preparación del Entorno Python ---&lt;br /&gt;
echo &amp;quot;--- 4. Configurando entorno Python e instalando requirements.txt ---&amp;quot;&lt;br /&gt;
cd $PROJECT_DIR&lt;br /&gt;
&lt;br /&gt;
# 4a. Crear y activar el entorno virtual&lt;br /&gt;
if [ ! -d &amp;quot;.venv&amp;quot; ]; then&lt;br /&gt;
    python3 -m venv .venv&lt;br /&gt;
fi&lt;br /&gt;
source .venv/bin/activate&lt;br /&gt;
&lt;br /&gt;
# 4b. Instalar dependencias&lt;br /&gt;
pip install -r requirements.txt&lt;br /&gt;
&lt;br /&gt;
# --- 5. Migraciones y Creación de Tablas ---&lt;br /&gt;
echo &amp;quot;--- 5. Ejecutando migraciones de Flask (Alembic) ---&amp;quot;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
&lt;br /&gt;
# 5a. Inicializar el repositorio de migraciones (si no existe)&lt;br /&gt;
if [ ! -d &amp;quot;migrations&amp;quot; ]; then&lt;br /&gt;
    flask db init&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
# 5b. Crear la migración inicial si es necesario (ESTA ES LA LÍNEA FALTANTE)&lt;br /&gt;
# Flask-Migrate solo crea tablas si existe un archivo de versión.&lt;br /&gt;
flask db migrate -m &amp;quot;Initial database migration&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# 5c. Aplicar las migraciones. Esto crea las tablas en la base de datos.&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;✅ Base de datos inicializada.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# --- 6. Lanzamiento de la Aplicación ---&lt;br /&gt;
echo &amp;quot;--- 6. Iniciando la aplicación Flask en segundo plano (Puerto 5000) ---&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Detener cualquier instancia previa para idempotencia&lt;br /&gt;
pkill -f 'flask run --host=0.0.0.0'&lt;br /&gt;
&lt;br /&gt;
# Iniciar la aplicación en segundo plano con nohup&lt;br /&gt;
# El .env se carga automáticamente por python-dotenv&lt;br /&gt;
nohup flask run --host=0.0.0.0 &amp;gt; /tmp/flask_app.log 2&amp;gt;&amp;amp;1 &amp;amp;&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;✅ Aprovisionamiento y lanzamiento completados. Accede en http://localhost:5000&amp;quot;&lt;br /&gt;
deactivate&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Explicación paso a paso del script de aprovisionamiento ===&lt;br /&gt;
&lt;br /&gt;
Aquí tienes la explicación detallada del script en texto, agrupada por pasos lógicos:&lt;br /&gt;
&lt;br /&gt;
==== Paso 1: Carga de Variables de Configuración ====&lt;br /&gt;
&lt;br /&gt;
Este bloque asegura que las credenciales de la base de datos y otras variables de entorno definidas en el archivo .env de tu máquina local estén disponibles dentro de la máquina virtual (VM).&lt;br /&gt;
    &lt;br /&gt;
&amp;lt;code&amp;gt;set -a&amp;lt;/code&amp;gt;: Este comando asegura que todas las variables definidas o modificadas a partir de este punto sean automáticamente exportadas al entorno (export all), haciéndolas accesibles para los comandos subsiguientes como mysql.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;source /vagrant/.env&amp;lt;/code&amp;gt;: Comando clave. El directorio /vagrant es el punto de montaje predeterminado que conecta el directorio de tu proyecto local con la VM. Este comando lee el archivo .env compartido e inserta sus variables ($DATABASE_USER, $DATABASE_DB, etc.) en el shell de la VM.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;set +a&amp;lt;/code&amp;gt; Desactiva la exportación automática de variables.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;PROJECT_DIR=&amp;quot;/vagrant&amp;quot;&amp;lt;/code&amp;gt;: Define una variable para el directorio raíz del proyecto, facilitando la navegación posterior.&lt;br /&gt;
&lt;br /&gt;
==== Paso 2: Instalación de Dependencias del Sistema ====&lt;br /&gt;
&lt;br /&gt;
Esta sección se encarga de instalar todo el software que necesita la aplicación para funcionar, asumiendo que la VM comienza con un sistema operativo limpio.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;sudo apt-get update&amp;lt;/code&amp;gt;: Actualiza la lista de paquetes disponibles en el sistema operativo.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;sudo apt-get install -y mariadb-server python3-pip python3-venv git python3-dev&amp;lt;/code&amp;gt;: Instala los paquetes esenciales sin pedir confirmación (-y):&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt; mariadb-server&amp;lt;/code&amp;gt;: El servidor de base de datos.&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;  python3-pip y python3-venv&amp;lt;/code&amp;gt;: Herramientas necesarias para gestionar paquetes y crear entornos virtuales de Python.&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt; git&amp;lt;/code&amp;gt;: Útil para gestión de código.&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt; python3-dev&amp;lt;/code&amp;gt;: Necesario para compilar bibliotecas de Python que tienen componentes nativos, como el conector de MariaDB (pymysql).&lt;br /&gt;
&lt;br /&gt;
==== Paso 3: Configuración de MariaDB ====&lt;br /&gt;
&lt;br /&gt;
Este bloque configura la base de datos y el usuario de manera idempotente (es decir, puede ejecutarse varias veces sin causar errores).&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt; sudo systemctl enable mariadb / sudo systemctl start mariadb&amp;lt;/code&amp;gt;: Asegura que el servicio MariaDB esté habilitado para arrancar con el sistema y lo inicia inmediatamente.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt; sleep 5&amp;lt;/code&amp;gt;: Espera 5 segundos para que el servicio de la base de datos se inicie completamente antes de intentar ejecutar comandos SQL.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt; sudo mysql -u root &amp;lt;&amp;lt;EOF ... EOF&amp;lt;/code&amp;gt;: Ejecuta los comandos SQL dentro del bloque.&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;CREATE DATABASE IF NOT EXISTS $DATABASE_DB ...&amp;lt;/code&amp;gt;: Crea la base de datos utilizando la variable cargada, si esta aún no existe.&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;CREATE USER IF NOT EXISTS '$DATABASE_USER'@'localhost' ...&amp;lt;/code&amp;gt;: Crea el usuario de la aplicación, también utilizando las variables cargadas.&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;GRANT ALL PRIVILEGES ON $DATABASE_DB.* TO '$DATABASE_USER'@'localhost'&amp;lt;/code&amp;gt;: Otorga todos los permisos al usuario recién creado, limitando su acceso a la base de datos de la aplicación.&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;FLUSH PRIVILEGES&amp;lt;/code&amp;gt;: Ordena a MariaDB que recargue la tabla de permisos inmediatamente.&lt;br /&gt;
&lt;br /&gt;
==== Paso 4: Preparación del Entorno Python ====&lt;br /&gt;
&lt;br /&gt;
En esta sección se configura el entorno de ejecución del proyecto.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;cd $PROJECT_DIR &amp;lt;/code&amp;gt;: Navega al directorio raíz del proyecto (/vagrant).&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;if [ ! -d &amp;quot;.venv&amp;quot; ]; then python3 -m venv .venv; fi &amp;lt;/code&amp;gt;: Crea el entorno virtual (.venv) si aún no existe (idempotencia).&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;source .venv/bin/activate &amp;lt;/code&amp;gt;: Activa el entorno virtual. Esto hace que los comandos como pip y flask apunten a los binarios dentro del .venv.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;pip install -r requirements.txt &amp;lt;/code&amp;gt;: Instala todas las dependencias de Python listadas en el archivo requirements.txt.&lt;br /&gt;
&lt;br /&gt;
==== Paso 5: Migraciones y Creación de Tablas ====&lt;br /&gt;
&lt;br /&gt;
Se utiliza Flask-Migrate (Alembic) para configurar la base de datos con el esquema de modelos de SQLAlchemy.&lt;br /&gt;
   &lt;br /&gt;
&amp;lt;code&amp;gt;export FLASK_APP=app.app:create_app&amp;lt;/code&amp;gt;: Establece la variable de entorno necesaria para que el comando flask sepa dónde encontrar la instancia de la aplicación (la función create_app en app/app.py).&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;if [ ! -d &amp;quot;migrations&amp;quot; ]; then flask db init; fi&amp;lt;/code&amp;gt;: Comprueba si el directorio migrations existe. Si no, inicializa el repositorio de migraciones de Alembic.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;flask db upgrade&amp;lt;/code&amp;gt;: Aplica la migración más reciente a la base de datos, lo que resulta en la creación de todas las tablas definidas por los modelos de la aplicación.&lt;br /&gt;
&lt;br /&gt;
==== Paso 6: Lanzamiento de la Aplicación ====&lt;br /&gt;
&lt;br /&gt;
Finalmente, se lanza la aplicación Flask y se sale del entorno de aprovisionamiento.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;pkill -f 'flask run --host=0.0.0.0'&amp;lt;/code&amp;gt;: Intenta detener cualquier proceso Flask anterior que pudiera estar ejecutándose, asegurando que solo haya una instancia activa.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;nohup flask run --host=0.0.0.0 &amp;gt; /tmp/flask_app.log 2&amp;gt;&amp;amp;1 &amp;amp;&amp;lt;/code&amp;gt;: Lanza el servidor Flask:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;--host=0.0.0.0&amp;lt;/code&amp;gt;: Permite que la aplicación sea accesible desde fuera de la VM (a través del puerto reenviado en Vagrantfile).&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt; nohup ... &amp;amp;&amp;lt;/code&amp;gt;: Ejecuta el comando en segundo plano (&amp;amp;) y lo hace inmune a colgarse (nohup), manteniendo el servidor activo incluso después de que termine la sesión de aprovisionamiento. La salida se redirige al archivo /tmp/flask_app.log.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt; deactivate&amp;lt;/code&amp;gt;: Sale del entorno virtual de Python.&lt;br /&gt;
&lt;br /&gt;
== Iniciar el Entorno Virtual con Vagrant ==&lt;br /&gt;
Desde la terminal, navega al directorio `flask_testing_project/` y ejecuta el siguiente comando:&lt;br /&gt;
 &amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
 vagrant up&lt;br /&gt;
 &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Cuando el proceso finalice, podrás acceder a la aplicación en `http://localhost:5000`.&lt;br /&gt;
&lt;br /&gt;
'''¿Has podido ver qué ocurría en cada paso? ¡Prueba a entrar en la máquina virtual y ver cómo está funcionando todo por dentro!'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
= Parte 2: Aprovisionamiento mediante un playbook de Ansible =&lt;br /&gt;
&lt;br /&gt;
El aprovisionamiento con Ansible ofrece varias ventajas frente al uso de scripts de shell. Mientras que los scripts de shell son imperativos, es decir, detallan los pasos exactos para lograr un objetivo, Ansible adopta un enfoque '''declarativo''', donde especificamos el estado deseado del sistema y Ansible se encarga de llevarlo a cabo. Esto permite una mayor '''idempotencia''', es decir, que la configuración se pueda aplicar varias veces sin causar efectos no deseados. Además, Ansible facilita la gestión de infraestructuras grandes de forma '''escalable y reproducible''', lo que hace que sea más fácil mantener entornos consistentes y automatizados en comparación con los scripts tradicionales. Un aspecto clave de Ansible es que permite aprovisionar diferentes máquinas sin importar el sistema operativo o la infraestructura subyacente, ya que simplemente le indicamos qué necesitamos y Ansible se encarga de implementar la configuración adecuada, independientemente de cómo o dónde se ejecute.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Paso 1: Modificar el Vagrantfile ==&lt;br /&gt;
&lt;br /&gt;
Modifica el Vagrantfile para ejecutar un playbook de Ansible en lugar del script provision.sh: &lt;br /&gt;
&lt;br /&gt;
 &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# Vagrantfile&lt;br /&gt;
# -*- mode: ruby -*-&lt;br /&gt;
# vi: set ft=ruby :&lt;br /&gt;
&lt;br /&gt;
Vagrant.configure(&amp;quot;2&amp;quot;) do |config|&lt;br /&gt;
  &lt;br /&gt;
  # 1. Sistema Operativo&lt;br /&gt;
  config.vm.box = &amp;quot;ubuntu/jammy64&amp;quot; &lt;br /&gt;
  &lt;br /&gt;
  # 2. Mapear puerto de Flask (usando 5001 como puerto de host para evitar colisiones)&lt;br /&gt;
  # Aplicación accesible en http://localhost:5001&lt;br /&gt;
  config.vm.network &amp;quot;forwarded_port&amp;quot;, guest: 5000, host: 5001&lt;br /&gt;
  &lt;br /&gt;
  # 3. Aprovisionamiento: Ejecuta el Playbook de Ansible&lt;br /&gt;
  config.vm.provision &amp;quot;ansible&amp;quot; do |ansible|&lt;br /&gt;
    # Especifica que Vagrant debe ejecutar el archivo playbook.yml&lt;br /&gt;
    ansible.playbook = &amp;quot;playbook.yml&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    # Ejecutar tareas con privilegios de root (sudo)&lt;br /&gt;
    ansible.become = true&lt;br /&gt;
    &lt;br /&gt;
    # Asegura que se ejecuta en el host de la MV&lt;br /&gt;
    ansible.limit = &amp;quot;all&amp;quot; &lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
 &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Paso 2: Crea el &amp;lt;code&amp;gt;playbook.yml&amp;lt;/code&amp;gt; ==&lt;br /&gt;
&lt;br /&gt;
En la raíz del proyecto, al mismo nivel que el Vagrantfile, crea el playbook.yml.&lt;br /&gt;
&lt;br /&gt;
 &amp;lt;syntaxhighlight lang=&amp;quot;yaml&amp;quot;&amp;gt;&lt;br /&gt;
---&lt;br /&gt;
- name: Configurar Entorno Flask y MariaDB&lt;br /&gt;
  hosts: all&lt;br /&gt;
  become: yes # Ejecutar tareas con privilegios de root (sudo)&lt;br /&gt;
  vars:&lt;br /&gt;
    project_dir: /vagrant&lt;br /&gt;
    env_file: &amp;quot;{{ project_dir }}/.env&amp;quot;&lt;br /&gt;
&lt;br /&gt;
  tasks:&lt;br /&gt;
    # --- 1. Saneamiento y Carga de Variables de Entorno (.env) ---&lt;br /&gt;
    - name: Cargar variables de entorno como JSON y establecer hechos&lt;br /&gt;
      ansible.builtin.shell: |&lt;br /&gt;
        # 1. Leer el archivo .env, limpiar CRLF (\r) y líneas vacías/comentarios.&lt;br /&gt;
        # 2. Convertir el formato KEY=VALUE al formato JSON &amp;quot;KEY&amp;quot;:&amp;quot;VALUE&amp;quot;.&lt;br /&gt;
        CLEAN_ENV=$(&lt;br /&gt;
          cat {{ env_file }} | &lt;br /&gt;
          tr -d '\r' | &lt;br /&gt;
          grep -vE '^\s*#' |&lt;br /&gt;
          grep -vE '^\s*$' | &lt;br /&gt;
          awk -F'=' '{ gsub(/^[[:space:]]+|[[:space:]]+$/, &amp;quot;&amp;quot;, $2); print &amp;quot;\&amp;quot;&amp;quot;$1&amp;quot;\&amp;quot;:\&amp;quot;&amp;quot;$2&amp;quot;\&amp;quot;&amp;quot; }' |&lt;br /&gt;
          paste -s -d ',' -&lt;br /&gt;
        )&lt;br /&gt;
        &lt;br /&gt;
        # 3. Imprimir el JSON completo. Ejemplo: {&amp;quot;KEY1&amp;quot;:&amp;quot;VALUE1&amp;quot;,&amp;quot;KEY1&amp;quot;:&amp;quot;VALUE2&amp;quot;}&lt;br /&gt;
        echo &amp;quot;{ $CLEAN_ENV }&amp;quot;&lt;br /&gt;
      register: env_output&lt;br /&gt;
      changed_when: false&lt;br /&gt;
      tags: [config, env]&lt;br /&gt;
&lt;br /&gt;
    - name: Establecer variables de entorno limpias como hechos de Ansible&lt;br /&gt;
      ansible.builtin.set_fact:&lt;br /&gt;
        env_vars: &amp;quot;{{ env_output.stdout | from_json }}&amp;quot;&lt;br /&gt;
      tags: [config, env]&lt;br /&gt;
      &lt;br /&gt;
    # --- 2. Preparación del Sistema ---&lt;br /&gt;
    - name: Forzar la actualización del índice de paquetes (apt update)&lt;br /&gt;
      ansible.builtin.apt:&lt;br /&gt;
        update_cache: yes&lt;br /&gt;
      tags: [install, base]&lt;br /&gt;
&lt;br /&gt;
    - name: Instalar MariaDB, Python y utilidades necesarias&lt;br /&gt;
      ansible.builtin.package:&lt;br /&gt;
        name: &lt;br /&gt;
          - mariadb-server&lt;br /&gt;
          - python3-pip &lt;br /&gt;
          - git&lt;br /&gt;
          - python3-dev&lt;br /&gt;
          - python3-venv&lt;br /&gt;
        state: present&lt;br /&gt;
      tags: [install, base]&lt;br /&gt;
&lt;br /&gt;
    - name: Instalar PyMySQL a nivel de sistema (Necesario para los módulos 'mysql_*' de Ansible)&lt;br /&gt;
      ansible.builtin.pip:&lt;br /&gt;
        name: pymysql&lt;br /&gt;
        executable: pip3&lt;br /&gt;
      tags: [install, db]&lt;br /&gt;
&lt;br /&gt;
    # --- 3. Configuración de MariaDB (Módulos declarativos de MySQL) ---&lt;br /&gt;
    - name: Asegurar que el servicio MariaDB esté iniciado y habilitado&lt;br /&gt;
      ansible.builtin.service:&lt;br /&gt;
        name: mariadb&lt;br /&gt;
        state: started&lt;br /&gt;
        enabled: yes&lt;br /&gt;
      tags: [db]&lt;br /&gt;
&lt;br /&gt;
    - name: Crear Base de Datos y Usuario&lt;br /&gt;
      block:&lt;br /&gt;
        - name: Crear Base de Datos&lt;br /&gt;
          community.mysql.mysql_db:&lt;br /&gt;
            name: &amp;quot;{{ env_vars.DATABASE_DB }}&amp;quot; &lt;br /&gt;
            state: present&lt;br /&gt;
            encoding: utf8mb4&lt;br /&gt;
            collation: utf8mb4_unicode_ci&lt;br /&gt;
            login_unix_socket: /run/mysqld/mysqld.sock &lt;br /&gt;
          tags: [db]&lt;br /&gt;
&lt;br /&gt;
        - name: Crear Usuario de Aplicación y Otorgar Permisos&lt;br /&gt;
          community.mysql.mysql_user:&lt;br /&gt;
            name: &amp;quot;{{ env_vars.DATABASE_USER }}&amp;quot;&lt;br /&gt;
            password: &amp;quot;{{ env_vars.DATABASE_PASSWORD }}&amp;quot;&lt;br /&gt;
            host: &amp;quot;localhost&amp;quot;&lt;br /&gt;
            priv: &amp;quot;{{ env_vars.DATABASE_DB }}.*:ALL&amp;quot; &lt;br /&gt;
            state: present&lt;br /&gt;
            login_unix_socket: /run/mysqld/mysqld.sock &lt;br /&gt;
          tags: [db]&lt;br /&gt;
&lt;br /&gt;
    # --- 4. Configuración de la Aplicación Flask ---&lt;br /&gt;
    - name: Instalar dependencias de Python a nivel de sistema&lt;br /&gt;
      ansible.builtin.pip:&lt;br /&gt;
        requirements: &amp;quot;{{ project_dir }}/requirements.txt&amp;quot;&lt;br /&gt;
        executable: pip3 # Usamos pip3 del sistema&lt;br /&gt;
        state: present&lt;br /&gt;
      tags: [app, install]&lt;br /&gt;
&lt;br /&gt;
    - name: Aplicar migraciones de base de datos (Flask-Migrate)&lt;br /&gt;
      ansible.builtin.command: python3 -m flask db upgrade # Usamos python3 -m flask&lt;br /&gt;
      environment:&lt;br /&gt;
        DATABASE_USER: &amp;quot;{{ env_vars.DATABASE_USER }}&amp;quot;&lt;br /&gt;
        DATABASE_PASSWORD: &amp;quot;{{ env_vars.DATABASE_PASSWORD }}&amp;quot;&lt;br /&gt;
        DATABASE_DB: &amp;quot;{{ env_vars.DATABASE_DB }}&amp;quot;&lt;br /&gt;
        DATABASE_HOST: &amp;quot;{{ env_vars.DATABASE_HOST | default('localhost') }}&amp;quot;&lt;br /&gt;
        FLASK_APP: app.app:create_app&lt;br /&gt;
      args:&lt;br /&gt;
        chdir: &amp;quot;{{ project_dir }}&amp;quot;&lt;br /&gt;
      become: yes&lt;br /&gt;
      become_user: vagrant&lt;br /&gt;
      tags: [app, db]&lt;br /&gt;
&lt;br /&gt;
    # --- 5. Lanzar la Aplicación ---&lt;br /&gt;
    &lt;br /&gt;
    # Tarea 5.1: Detener procesos previos&lt;br /&gt;
    - name: Detener instancias de Flask previamente en ejecución&lt;br /&gt;
      ansible.builtin.shell: &amp;quot;pkill -f 'python3 -m flask run --host=0.0.0.0'&amp;quot;&lt;br /&gt;
      become: yes&lt;br /&gt;
      become_user: vagrant&lt;br /&gt;
      failed_when: false # CRÍTICO: Ignorar el error si pkill no encuentra nada.&lt;br /&gt;
      tags: [app, run]&lt;br /&gt;
&lt;br /&gt;
    # Tarea 5.2: Iniciar Flask en segundo plano con nohup&lt;br /&gt;
    - name: Iniciar Flask en segundo plano con nohup&lt;br /&gt;
      ansible.builtin.shell: |&lt;br /&gt;
        nohup python3 -m flask run --host=0.0.0.0 &amp;gt; /tmp/flask_app.log 2&amp;gt;&amp;amp;1 &amp;amp;&lt;br /&gt;
      environment:&lt;br /&gt;
        FLASK_APP: app.app:create_app&lt;br /&gt;
        DATABASE_USER: &amp;quot;{{ env_vars.DATABASE_USER }}&amp;quot;&lt;br /&gt;
        DATABASE_PASSWORD: &amp;quot;{{ env_vars.DATABASE_PASSWORD }}&amp;quot;&lt;br /&gt;
        DATABASE_DB: &amp;quot;{{ env_vars.DATABASE_DB }}&amp;quot;&lt;br /&gt;
        DATABASE_HOST: &amp;quot;{{ env_vars.DATABASE_HOST | default('localhost') }}&amp;quot;&lt;br /&gt;
      args:&lt;br /&gt;
        chdir: &amp;quot;{{ project_dir }}&amp;quot;&lt;br /&gt;
      become: yes&lt;br /&gt;
      become_user: vagrant&lt;br /&gt;
      tags: [app, run]&lt;br /&gt;
&lt;br /&gt;
    - name: Verificar que Flask se esté ejecutando&lt;br /&gt;
      ansible.builtin.shell: &amp;quot;ps aux | grep -v grep | grep 'python3 -m flask run --host=0.0.0.0'&amp;quot;&lt;br /&gt;
      register: flask_status&lt;br /&gt;
      failed_when: flask_status.rc != 0 and 'python3 -m flask run' not in flask_status.stdout&lt;br /&gt;
      tags: [app, run]&lt;br /&gt;
&lt;br /&gt;
    - name: Mostrar mensaje de acceso&lt;br /&gt;
      ansible.builtin.debug:&lt;br /&gt;
        msg: &amp;quot;✅ ¡Aprovisionamiento COMPLETO! La aplicación Flask está en segundo plano. Acceda en http://localhost:5001&amp;quot;&lt;br /&gt;
      tags: [app, run]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
 &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Paso 2: Explicación detallada del &amp;lt;code&amp;gt;playbook.yml&amp;lt;/code&amp;gt; ===&lt;br /&gt;
&lt;br /&gt;
==== Carga y Saneamiento de Variables ====&lt;br /&gt;
&lt;br /&gt;
Este bloque es fundamental para la estabilidad. Resuelve los problemas de caracteres invisibles del archivo &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt; que suelen causar fallos en MariaDB, cargando las variables en un formato JSON limpio.&lt;br /&gt;
&lt;br /&gt;
Ansible lee el archivo &amp;lt;code&amp;gt;/vagrant/.env&amp;lt;/code&amp;gt;, utiliza herramientas de shell como &amp;lt;code&amp;gt;tr&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;awk&amp;lt;/code&amp;gt; para eliminar cualquier carácter de retorno de carro (CRLF) o espacio sobrante, y lo formatea como un objeto JSON.&lt;br /&gt;
&lt;br /&gt;
El módulo &amp;lt;code&amp;gt;set_fact&amp;lt;/code&amp;gt; carga este JSON limpio en la variable &amp;lt;code&amp;gt;env_vars&amp;lt;/code&amp;gt;, garantizando que nombres como la base de datos y el usuario sean cadenas de texto perfectas.&lt;br /&gt;
&lt;br /&gt;
==== Instalación de Componentes Base ====&lt;br /&gt;
&lt;br /&gt;
Esta sección configura el sistema operativo para ejecutar la base de datos y la aplicación Python.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;sudo apt update&amp;lt;/code&amp;gt;: Se fuerza la actualización del índice de paquetes para asegurar que se instalen las versiones más recientes.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;sudo apt install mariadb-server python3-pip python3-dev ...&amp;lt;/code&amp;gt;: Instala todos los paquetes necesarios del sistema: el servidor de MariaDB, las herramientas de Python 3, y las cabeceras de desarrollo de Python (&amp;lt;code&amp;gt;python3-dev&amp;lt;/code&amp;gt;), que son necesarias para compilar bibliotecas binarias como mysqlclient.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;pip3 install pymysql&amp;lt;/code&amp;gt;: Instala la biblioteca PyMySQL a nivel de sistema. Esta biblioteca no es para la aplicación Flask, sino para que Ansible pueda comunicarse con MariaDB y crear la base de datos y el usuario en las siguientes tareas.&lt;br /&gt;
&lt;br /&gt;
==== Configuración de MariaDB ====&lt;br /&gt;
&lt;br /&gt;
Este bloque configura la base de datos y el usuario de manera idempotente utilizando los módulos declarativos de Ansible, que reemplazan la necesidad de ejecutar comandos SQL manuales.&lt;br /&gt;
&lt;br /&gt;
La tarea &amp;lt;code&amp;gt;Asegurar que el servicio MariaDB esté iniciado y habilitado&amp;lt;/code&amp;gt; equivale a:&lt;br /&gt;
&amp;lt;code&amp;gt;sudo systemctl enable mariadb / sudo systemctl start mariadb&amp;lt;/code&amp;gt;: Asegura que el servicio MariaDB esté habilitado para arrancar con el sistema y lo inicia inmediatamente.&lt;br /&gt;
&lt;br /&gt;
La tarea &amp;lt;code&amp;gt;Crear Base de Datos&amp;lt;/code&amp;gt; utiliza el valor limpio de &amp;lt;code&amp;gt;DATABASE_DB&amp;lt;/code&amp;gt; para crearla con la codificación correcta (utf8mb4).&lt;br /&gt;
&lt;br /&gt;
La tarea &amp;lt;code&amp;gt;Crear Usuario de Aplicación y Otorgar Permisos&amp;lt;/code&amp;gt; utiliza los valores limpios de &amp;lt;code&amp;gt;DATABASE_USER&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;DATABASE_PASSWORD&amp;lt;/code&amp;gt; para crear el usuario y otorgarle todos los permisos (&amp;lt;code&amp;gt;GRANT ALL PRIVILEGES&amp;lt;/code&amp;gt;) sobre la base de datos recién creada.&lt;br /&gt;
&lt;br /&gt;
==== Instalación de Dependencias Python  ====&lt;br /&gt;
&lt;br /&gt;
La tarea &amp;lt;code&amp;gt;Instalar dependencias de Python a nivel de sistema&amp;lt;/code&amp;gt; equivale a:&lt;br /&gt;
&amp;lt;code&amp;gt;sudo pip3 install -r /vagrant/requirements.txt&amp;lt;/code&amp;gt;: Instala todas las dependencias listadas en el archivo &amp;lt;code&amp;gt;requirements.txt&amp;lt;/code&amp;gt; a nivel global del sistema.&lt;br /&gt;
&lt;br /&gt;
==== Migraciones y Creación de Tablas ====&lt;br /&gt;
&lt;br /&gt;
Se utiliza Flask-Migrate para configurar la base de datos con el esquema de modelos de SQLAlchemy.&lt;br /&gt;
&lt;br /&gt;
La tarea &amp;lt;code&amp;gt;Aplicar migraciones de base de datos (Flask-Migrate)&amp;lt;/code&amp;gt; ejecuta el comando: &amp;lt;code&amp;gt;export FLASK_APP=app.app:create_app&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;python3 -m flask db upgrade&amp;lt;/code&amp;gt;: Aplica la migración más reciente a la base de datos, creando todas las tablas definidas por los modelos de la aplicación (o actualizándolas si ya existen). Esta tarea se ejecuta como el usuario &amp;lt;code&amp;gt;vagrant&amp;lt;/code&amp;gt; para asegurar los permisos de acceso a los archivos del proyecto.&lt;br /&gt;
&lt;br /&gt;
==== Lanzamiento de la Aplicación ====&lt;br /&gt;
&lt;br /&gt;
Finalmente, se lanza la aplicación Flask y se asegura su continuidad.&lt;br /&gt;
&lt;br /&gt;
La tarea &amp;lt;code&amp;gt;Detener instancias de Flask previamente en ejecución&amp;lt;/code&amp;gt; ejecuta:&lt;br /&gt;
&amp;lt;code&amp;gt;pkill -f 'python3 -m flask run --host=0.0.0.0'&amp;lt;/code&amp;gt;: Intenta detener cualquier proceso Flask anterior. Es crucial que la tarea tenga la opción &amp;lt;code&amp;gt;failed_when: false&amp;lt;/code&amp;gt;, para que Ansible no falle si no se encuentra ningún proceso que matar.&lt;br /&gt;
&lt;br /&gt;
La tarea &amp;lt;code&amp;gt;Iniciar Flask en segundo plano con nohup&amp;lt;/code&amp;gt; ejecuta:&lt;br /&gt;
&amp;lt;code&amp;gt;nohup python3 -m flask run --host=0.0.0.0 &amp;gt; /tmp/flask_app.log 2&amp;gt;&amp;amp;1 &amp;amp;&amp;lt;/code&amp;gt;: Lanza el servidor Flask:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;--host=0.0.0.0&amp;lt;/code&amp;gt;: Permite que la aplicación sea accesible desde fuera de la VM (a través del puerto reenviado en Vagrantfile).&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;nohup ... &amp;amp;&amp;lt;/code&amp;gt;: Ejecuta el comando en segundo plano (&amp;lt;code&amp;gt;&amp;amp;&amp;lt;/code&amp;gt;) y lo hace inmune a colgarse (&amp;lt;code&amp;gt;nohup&amp;lt;/code&amp;gt;), manteniendo el servidor activo. La salida se redirige al archivo &amp;lt;code&amp;gt;/tmp/flask_app.log&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Verificación y Mensaje Final: Las últimas tareas verifican que el proceso Flask esté corriendo y muestran un mensaje de éxito, indicando que la aplicación está lista en &amp;lt;code&amp;gt;http://localhost:5001&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
La aplicación Flask estará disponible en `http://localhost:5001` cuando finalice el aprovisionamiento.&lt;br /&gt;
&lt;br /&gt;
'''¿Qué has podido observar ahora en la terminal en la etapa de aprovisionamiento? ¿Crees que la MV estará funcionando igual que en el aprovisionamiento por shell script?'''&lt;br /&gt;
&lt;br /&gt;
=Conclusión=&lt;br /&gt;
&lt;br /&gt;
Este tutorial te ha guiado a través de dos métodos de aprovisionamiento de una aplicación Flask en Vagrant, utilizando un script de shell y un playbook de Ansible. ¡Ahora deberías tener una mejor comprensión del aprovisionamiento automático y de cómo desplegar aplicaciones en entornos virtuales reproducibles!&lt;br /&gt;
&lt;br /&gt;
¡Enhorabuena si has llegado hasta aquí! Y, recuerda, las máquinas virtuales consumen recursos de tu equipo, apágalas y elimínalas cuando ya no las necesites.&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Archivo:EGC_2025-26_P6.pdf&amp;diff=10221</id>
		<title>Archivo:EGC 2025-26 P6.pdf</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Archivo:EGC_2025-26_P6.pdf&amp;diff=10221"/>
				<updated>2025-11-18T06:16:47Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: Jmorenol subió una nueva versión de Archivo:EGC 2025-26 P6.pdf&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Pr%C3%A1cticas_-_25/26&amp;diff=10220</id>
		<title>Prácticas - 25/26</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Pr%C3%A1cticas_-_25/26&amp;diff=10220"/>
				<updated>2025-11-18T05:49:31Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[Página_Principal]] -&amp;gt; [[2025/2026]] -&amp;gt; [[Prácticas - 25/26]]&lt;br /&gt;
&lt;br /&gt;
== Prácticas ==&lt;br /&gt;
&lt;br /&gt;
* Práctica 1: '''Instalación del sistema base''' [[Archivo:EGC 2025-26 P1.pdf]]&lt;br /&gt;
* Práctica 2: '''Integración y despliegue continuos ''' [[Archivo:EGC 2025-26 P2.pdf]]&lt;br /&gt;
** [[Workflows solución P2 2526 |Workflows solución para los ejercicios propuestos]]&lt;br /&gt;
* Práctica 3: '''Gestión del código fuente''' [[Archivo:EGC_2025-26_P3.pdf]]&lt;br /&gt;
** Solución ejercicios alumno/a A: [[Archivo:P3 usuario A 2526.pdf]]&lt;br /&gt;
** Solución ejercicios alumno/a B: [[Archivo:P3 usuario B 2526.pdf]]&lt;br /&gt;
* Práctica 4: '''Automatización de pruebas''' [[Archivo:EGC_2025-26_P4.pdf]]&lt;br /&gt;
* Práctica 5: '''Contenedores, dev-containers y aislamiento''' [[Archivo:EGC_2025-26_P5.pdf]]&lt;br /&gt;
*Práctica 6: '''Máquinas virtuales y aprovisionamiento''' [[Archivo:EGC 2025-26 P6.pdf ]]&lt;br /&gt;
** Campo de entrenamiento de Vagrant: [[Tutorial Campo de entrenamiento de Vagrant]]&lt;br /&gt;
** Tutorial configurando una máquina virtual para una aplicación: [[Tutorial configurando vagrant para una aplicación 2526|Tutorial configurando vagrant para una aplicación]]&lt;br /&gt;
** ¡Ahora es el momento de probar con uvlhub! [https://docs.uvlhub.io/installation/installation_with_vagrant]&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
==== Material adicional para el desarrollo de las sesiones prácticas ====&lt;br /&gt;
&lt;br /&gt;
* Box con la imagen de Ubuntu de los equipos de laboratorio de prácticas : [https://portal.cloud.hashicorp.com/vagrant/discover/cdcetsii/UbuntuEGC_25]&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Pr%C3%A1cticas_-_25/26&amp;diff=10186</id>
		<title>Prácticas - 25/26</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Pr%C3%A1cticas_-_25/26&amp;diff=10186"/>
				<updated>2025-10-28T07:17:18Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Prácticas */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[Página_Principal]] -&amp;gt; [[2025/2026]] -&amp;gt; [[Prácticas - 25/26]]&lt;br /&gt;
&lt;br /&gt;
== Prácticas ==&lt;br /&gt;
&lt;br /&gt;
* Práctica 1: '''Instalación del sistema base''' [[Archivo:EGC 2025-26 P1.pdf]]&lt;br /&gt;
* Práctica 2: '''Integración y despliegue continuos ''' [[Archivo:EGC 2025-26 P2.pdf]]&lt;br /&gt;
** [[Workflows solución P2 2526 |Workflows solución para los ejercicios propuestos]]&lt;br /&gt;
* Práctica 3: '''Gestión del código fuente''' [[Archivo:EGC_2025-26_P3.pdf]]&lt;br /&gt;
** Solución ejercicios alumno/a A: [[Archivo:P3 usuario A 2526.pdf]]&lt;br /&gt;
** Solución ejercicios alumno/a B: [[Archivo:P3 usuario B 2526.pdf]]&lt;br /&gt;
* Práctica 4: '''Automatización de pruebas''' [[Archivo:EGC_2025-26_P4.pdf]]&lt;br /&gt;
* Práctica 5: '''Contenedores, dev-containers y aislamiento''' [[Archivo:EGC_2025-26_P5.pdf]]&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
==== Material adicional para el desarrollo de las sesiones prácticas ====&lt;br /&gt;
&lt;br /&gt;
* Box con la imagen de Ubuntu de los equipos de laboratorio de prácticas : [https://portal.cloud.hashicorp.com/vagrant/discover/cdcetsii/UbuntuEGC_25]&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Archivo:EGC_2025-26_P5.pdf&amp;diff=10185</id>
		<title>Archivo:EGC 2025-26 P5.pdf</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Archivo:EGC_2025-26_P5.pdf&amp;diff=10185"/>
				<updated>2025-10-28T07:16:58Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10184</id>
		<title>Tutorial-docker</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial-docker&amp;diff=10184"/>
				<updated>2025-10-28T07:12:59Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Orquestación básica */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Ejercicio 1: Empezamos desde cero =&lt;br /&gt;
&lt;br /&gt;
El objetivo es eliminar toda la configuración previa y crear desde cero la estructura de carpetas que contendrá nuestros futuros archivos Docker.&lt;br /&gt;
&lt;br /&gt;
== Eliminar la configuración existente ==&lt;br /&gt;
Antes de comenzar, asegúrate de borrar cualquier carpeta Docker existente en el proyecto. En este caso, le haremos una copia de seguridad para poder volver a ella.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mv docker docker.bk&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
También debes asegurarte que trabajas con las variables de entorno adecuadas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cp .env.docker.example .env&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
⚠️ Este paso es importante: partimos completamente desde cero para entender cada parte del sistema.&lt;br /&gt;
&lt;br /&gt;
== Instalar Docker y Docker Compose (Linux) ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install -y apt-transport-https ca-certificates curl software-properties-common&lt;br /&gt;
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg&lt;br /&gt;
echo &amp;quot;deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable&amp;quot; | sudo tee /etc/apt/sources.list.d/docker.list &amp;gt; /dev/null&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install -y docker-ce&lt;br /&gt;
sudo usermod -aG docker ${USER}&lt;br /&gt;
mkdir -p ~/.docker/cli-plugins/&lt;br /&gt;
LATEST_VERSION=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep '&amp;quot;tag_name&amp;quot;:' | sed -E 's/.*&amp;quot;([^&amp;quot;]+)&amp;quot;.*/\1/')&lt;br /&gt;
curl -SL &amp;quot;https://github.com/docker/compose/releases/download/${LATEST_VERSION}/docker-compose-$(uname -s)-$(uname -m)&amp;quot; -o ~/.docker/cli-plugins/docker-compose&lt;br /&gt;
chmod +x ~/.docker/cli-plugins/docker-compose&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Es posible que tengas que reiniciar la sesión de usuario para poder usar docker sin necesidad de sudo.&lt;br /&gt;
&lt;br /&gt;
== Crear un Dockerfile mínimo ==&lt;br /&gt;
&lt;br /&gt;
Ahora crea nuevamente la carpeta base donde almacenaremos los archivos relacionados con Docker:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt; mkdir -p docker/images &amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Creamos un Dockerfile vacío&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
touch docker/images/Dockerfile&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vamos a explicar los distintos apartados:&lt;br /&gt;
&lt;br /&gt;
=== FROM ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Indica la imagen base sobre la que se construirá la tuya.&lt;br /&gt;
En este caso, parte de una imagen ligera que ya tiene Python 3.12 instalado.&lt;br /&gt;
&lt;br /&gt;
=== WORKDIR ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta línea define el directorio de trabajo dentro del contenedor.&lt;br /&gt;
A partir de aquí, todos los comandos que aparezcan después (COPY, RUN, CMD, etc.) se ejecutarán dentro de /app.&lt;br /&gt;
&lt;br /&gt;
'''Ojo: /app no es la carpeta app/ de tu proyecto local.'''. Son cosas distintas.&lt;br /&gt;
&lt;br /&gt;
En tu máquina puede existir algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
uvlhub&lt;br /&gt;
   app&lt;br /&gt;
   core&lt;br /&gt;
   requirements.txt &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Pero dentro del contenedor la estructura será distinta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
/ &lt;br /&gt;
  app/ &lt;br /&gt;
    app/ ← aquí dentro se habrá copiado tu carpeta local &amp;quot;app&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Cuando más adelante uses:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Docker copiará todos los archivos de tu proyecto dentro de la carpeta /app del contenedor.&lt;br /&gt;
Por eso el código quedará en /app/app/.&lt;br /&gt;
&lt;br /&gt;
=== COPY ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
COPY . .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Copia todos los archivos del proyecto (del host) dentro del contenedor, en /app.&lt;br /&gt;
El primer punto es el origen; el segundo, el destino.&lt;br /&gt;
&lt;br /&gt;
=== RUN ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ejecuta comandos durante la construcción de la imagen.&lt;br /&gt;
Aquí instalamos las dependencias necesarias desde requirements.txt.&lt;br /&gt;
El resultado de este paso quedará guardado en la imagen.&lt;br /&gt;
&lt;br /&gt;
=== EXPOSE ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Documenta el puerto que la aplicación usa dentro del contenedor.&lt;br /&gt;
Esto no lo abre al exterior; simplemente indica que la app escucha en el 5000.&lt;br /&gt;
&lt;br /&gt;
=== CMD ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Define el comando que se ejecutará cuando el contenedor se inicie.&lt;br /&gt;
En este caso, ejecutará la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
=== RUN vs CMD ===&lt;br /&gt;
&lt;br /&gt;
Estas dos instrucciones parecen similares, pero hacen cosas muy distintas.&lt;br /&gt;
&lt;br /&gt;
==== RUN ====&lt;br /&gt;
Se ejecuta durante la construcción de la imagen (en el momento del docker build).&lt;br /&gt;
Cada RUN crea una nueva capa dentro de la imagen.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
RUN pip install -r requirements.txt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando instala las dependencias una sola vez, cuando se construye la imagen.&lt;br /&gt;
El resultado (las librerías instaladas) queda grabado dentro de la imagen final.&lt;br /&gt;
&lt;br /&gt;
Si después lanzas un contenedor nuevo a partir de esa imagen, las dependencias ya estarán instaladas y no se volverán a ejecutar.&lt;br /&gt;
&lt;br /&gt;
==== CMD ====&lt;br /&gt;
Se ejecuta cuando se inicia el contenedor (en el docker run).&lt;br /&gt;
Define el proceso principal que se ejecutará dentro del contenedor mientras esté activo.&lt;br /&gt;
&lt;br /&gt;
Ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando no se ejecuta durante la construcción, sino cada vez que arrancas el contenedor.&lt;br /&gt;
Si detienes el contenedor, este proceso también se detiene.&lt;br /&gt;
&lt;br /&gt;
==== Diferencia resumida ====&lt;br /&gt;
&lt;br /&gt;
RUN → se ejecuta al construir la imagen (fase de docker build).&lt;br /&gt;
&lt;br /&gt;
CMD → se ejecuta al iniciar el contenedor (fase de docker run).&lt;br /&gt;
&lt;br /&gt;
Solo puede haber un CMD por Dockerfile.&lt;br /&gt;
Si hay más de uno, solo se usará el último.&lt;br /&gt;
&lt;br /&gt;
== Dockerfile completo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM python:3.12-slim&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
COPY . .&lt;br /&gt;
RUN pip install --no-cache-dir -r requirements.txt&lt;br /&gt;
EXPOSE 5000&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto tiene que ir en un Dockerfile en docker/images&lt;br /&gt;
&lt;br /&gt;
== Construir la imagen ==&lt;br /&gt;
Desde la raíz del proyecto (no dentro de docker/images):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker build -t uvlhub:dev -f docker/images/Dockerfile .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Ejecutar el contenedor ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Qué es lo que observas?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 2: Conectando servicios =&lt;br /&gt;
&lt;br /&gt;
¡Vaya! ¡No funciona! Can't connect to MySQL server on 'db'... ¡Ah, claro, intenta conectarse a la base de datos, que no lo tenemos! Pero claro, en un contenedor no es recomendable tener la app y la base de datos conviviendo...&lt;br /&gt;
&lt;br /&gt;
== Crear contenedor MariaDB ==&lt;br /&gt;
&lt;br /&gt;
Antes que nada, haz Ctrl + C para parar el contenedor web. En este ejercicio aprenderás a lanzar un contenedor de MariaDB 12.0.2 de forma manual, configurando todas las variables necesarias para tu aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
El objetivo es entender la complejidad de hacerlo “a mano”, antes de automatizarlo con docker-compose.&lt;br /&gt;
&lt;br /&gt;
== Descargar la imagen oficial ==&lt;br /&gt;
Primero descarga la versión exacta que usaremos:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker pull mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Crear el contenedor de MariaDB ==&lt;br /&gt;
Lanza un contenedor con todas las variables necesarias para que Flask pueda conectarse correctamente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d --name mariadb_container \&lt;br /&gt;
 -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
 -e FLASK_ENV=development \&lt;br /&gt;
 -e DOMAIN=localhost \&lt;br /&gt;
 -e MARIADB_HOSTNAME=db \&lt;br /&gt;
 -e MARIADB_PORT=3306 \&lt;br /&gt;
 -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
 -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
 -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
 -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
 -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
 -e WORKING_DIR=/app/ \&lt;br /&gt;
 -p 3306:3306 mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Explicación:&lt;br /&gt;
&lt;br /&gt;
* -d: ejecuta el contenedor en segundo plano (modo detached).&lt;br /&gt;
* --name: asigna un nombre identificable al contenedor.&lt;br /&gt;
* -e: define variables de entorno dentro del contenedor.&lt;br /&gt;
* -p 3306:3306: publica el puerto de MariaDB en tu máquina local.&lt;br /&gt;
* mariadb:12.0.2: imagen exacta que vamos a usar.&lt;br /&gt;
&lt;br /&gt;
Aquí ya, de entrada, vemos que esto es más feo que una nevera por detrás. Para empezar, estamos pasando a lo loco un montón de variables que difílmente será replicable en producción, por contener información sensible. Aparte, si metemos nuevas variables, ese comando queda inservible. Necesitaremos una solución más ágil para este problema&lt;br /&gt;
&lt;br /&gt;
== Comprobar que está corriendo ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver algo así:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES&lt;br /&gt;
abcd1234efgh mariadb:12.0.2 &amp;quot;docker-entrypoint.s…&amp;quot; Up 5 seconds 0.0.0.0:3306-&amp;gt;3306/tcp mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Conectarse a la base de datos ==&lt;br /&gt;
Abre una consola dentro del contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it mariadb_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y dentro, accede al cliente de MariaDB:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mariadb -u root -p&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Contraseña: uvlhubdb_root_password&lt;br /&gt;
&lt;br /&gt;
== Verificar la base de datos ==&lt;br /&gt;
Una vez dentro del cliente MySQL, ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
SHOW DATABASES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deberías ver que existen la bases de datos uvlhubdb porque pedimos crearla a la hora de levantar el contenedor. ¿Qué tiene la base de datos dentro? Vamos a verlo...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot;&amp;gt;&lt;br /&gt;
USE uvlhubdb;&lt;br /&gt;
SHOW TABLES;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Un total de... 0 tablas... ¡Claro! Es que solo hemos creado la base en sí, pero no las tablas. ¿Con un script SQL tal vez? ¡Espera, espera, si tenemos las migraciones! Pero claro, para eso necesito el contenedor de la app levantado. Sal de la consola SQL y del contenedor de MariaDB. Bien, ahora en el host...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta vez con el flag &amp;quot;-d&amp;quot; para que corra en segundo plano. Ahora, accedamos al contenedor y ejecutemos las migraciones. Pero... ¿cómo se llama mi contenedor?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que Docker asigna un nombre aleatorio a tu contenedor web porque no definiste ninguno. Una vez identificado el nombre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it &amp;lt;nombre&amp;gt; bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
What? ¿Qué pasa? ¿Otra vez error de conexión con la base? Si tenemos el contenedor funcionando, esto es un jaleo...&lt;br /&gt;
&lt;br /&gt;
== Redes internas ==&lt;br /&gt;
&lt;br /&gt;
Ahora mismo tienes dos contenedores aislados, el de la app y el la base de datos. Por defecto, cada contenedor tiene su propia red interna y no puede ver a los demás por nombre. El hostname '''db''' que usa Flask no existe en su DNS interno.&lt;br /&gt;
&lt;br /&gt;
=== Conectar servicios ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una red interna que compartan ambos contenedores&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker network create uvlhub_network&lt;br /&gt;
docker network ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Vemos que ya aparece nuestra red '''uvlhub_network'''. Ahora, conectemos el contenedor de MariaDB a la red:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Claro, si te fijas, ahora hay que añadir, además de toda esa configuración engorrosa, que se añada en la misma red y que además tenga de hostname &amp;quot;db&amp;quot; para que el contenedor web entienda qué es &amp;quot;db&amp;quot;. Tenemos que hacer lo mismo para el contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop &amp;lt;nombre&amp;gt;&lt;br /&gt;
docker rm &amp;lt;nombre&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y lo levantamos de nuevo, pero ya en la nueva red. De paso, le ponemos un nombre para identificarlo mejor.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -p 5000:5000 --name web_app_container --network uvlhub_network -d uvlhub:dev&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y ahora ya podemos acceder al contenedor por su nombre y ejecutar las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre el navegador en [http://localhost:5000 http://localhost:5000]. ¿Mejor?&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 3: Persistencia =&lt;br /&gt;
&lt;br /&gt;
Vamos a simular que hemos desplegado justo esto en producción y el servidor se reinicia por un corte de luz. Reiniciemos a mano cada contenedor:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
docker restart mariadb_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿Cómo? ¿Qué ha pasado? ¿Otra vez problema de conexión? Ve dándole a F5... F5... ¡bingo! Ahora sí sale, pero menuda aleatoriedad, ¿no? Claro, es que ahora caes en que una cosa es que el contenedor haya arrancado y otra que esté listo.&lt;br /&gt;
&lt;br /&gt;
Aparte, tenemos otro problema. ¿Qué pasa con la persistencia? Porque si borro el contenedor de MariaDB, todos los datos que estén en su interior también se pierden. Y si estamos en producción, lío asegurado.&lt;br /&gt;
&lt;br /&gt;
== Volúmenes ==&lt;br /&gt;
&lt;br /&gt;
Cada vez que eliminas el contenedor de MariaDB, todas las bases de datos se pierden. Esto ocurre porque los datos se guardan dentro del sistema de archivos del contenedor, y al borrarlo desaparecen con él.&lt;br /&gt;
&lt;br /&gt;
En este ejercicio creas un volumen de Docker para que los datos persistan entre ejecuciones.&lt;br /&gt;
&lt;br /&gt;
=== Crear un volumen ===&lt;br /&gt;
Un volumen es una carpeta gestionada por Docker que vive fuera del ciclo de vida de los contenedores.&lt;br /&gt;
&lt;br /&gt;
Ejecuta:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume create mariadb_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba que existe:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La salida muestra algo como:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
DRIVER    VOLUME NAME&lt;br /&gt;
local     mariadb_data&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Crear un contenedor de MariaDB con el volumen ===&lt;br /&gt;
Lanza el contenedor y conecta el volumen a la ruta donde MariaDB guarda los datos (/var/lib/mysql):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop mariadb_container&lt;br /&gt;
docker rm mariadb_container&lt;br /&gt;
docker run -d \&lt;br /&gt;
  --name mariadb_container \&lt;br /&gt;
  --hostname db \&lt;br /&gt;
  --network uvlhub_network \&lt;br /&gt;
  -v mariadb_data:/var/lib/mysql \&lt;br /&gt;
  -e FLASK_APP_NAME=&amp;quot;UVLHUB.IO(dev)&amp;quot; \&lt;br /&gt;
  -e FLASK_ENV=development \&lt;br /&gt;
  -e DOMAIN=localhost \&lt;br /&gt;
  -e MARIADB_HOSTNAME=db \&lt;br /&gt;
  -e MARIADB_PORT=3306 \&lt;br /&gt;
  -e MARIADB_DATABASE=uvlhubdb \&lt;br /&gt;
  -e MARIADB_TEST_DATABASE=uvlhubdb_test \&lt;br /&gt;
  -e MARIADB_USER=uvlhubdb_user \&lt;br /&gt;
  -e MARIADB_PASSWORD=uvlhubdb_password \&lt;br /&gt;
  -e MARIADB_ROOT_PASSWORD=uvlhubdb_root_password \&lt;br /&gt;
  -e WORKING_DIR=/app/ \&lt;br /&gt;
  -p 3306:3306 \&lt;br /&gt;
  mariadb:12.0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Qué significa &amp;quot;:&amp;quot; en los volúmenes ===&lt;br /&gt;
&lt;br /&gt;
Cuando usas la opción '''-v''' o '''--volume''' en Docker, el signo dos puntos (''':''') separa dos rutas:&lt;br /&gt;
&lt;br /&gt;
'''-v origen:destino'''&lt;br /&gt;
&lt;br /&gt;
origen → es la ruta o el volumen en tu máquina (el host).&lt;br /&gt;
&lt;br /&gt;
destino → es la ruta dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 4: Orquestación de servicios =&lt;br /&gt;
&lt;br /&gt;
Hasta ahora hemos creado y conectado los contenedores manualmente.&lt;br /&gt;
Ahora vas a usar Docker Compose para definir todo en un solo archivo.&lt;br /&gt;
&lt;br /&gt;
Lo primero es parar y eliminar los contenedores previos para que no interfieran:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop $(docker ps -a -q)&lt;br /&gt;
docker rm $(docker ps -a -q)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Orquestación básica ==&lt;br /&gt;
&lt;br /&gt;
Crea un archivo llamado docker-compose.yml en la carpeta '''docker''' con este contenido:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    container_name: web_app_container&lt;br /&gt;
    env_file:&lt;br /&gt;
      - ../.env&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;5000&amp;quot;&lt;br /&gt;
    depends_on:&lt;br /&gt;
      - db&lt;br /&gt;
    build:&lt;br /&gt;
      context: ../&lt;br /&gt;
      dockerfile: docker/images/Dockerfile&lt;br /&gt;
    networks:&lt;br /&gt;
      - uvlhub_network&lt;br /&gt;
&lt;br /&gt;
  db:&lt;br /&gt;
    container_name: mariadb_container&lt;br /&gt;
    image: mariadb:12.0.2&lt;br /&gt;
    env_file:&lt;br /&gt;
      - ../.env&lt;br /&gt;
    volumes:&lt;br /&gt;
      - db_data:/var/lib/mysql&lt;br /&gt;
    networks:&lt;br /&gt;
      - uvlhub_network&lt;br /&gt;
&lt;br /&gt;
volumes:&lt;br /&gt;
  db_data:&lt;br /&gt;
&lt;br /&gt;
networks:&lt;br /&gt;
  uvlhub_network:&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate que esta orquestación de servicios me permite definir el nombre de cada servicio, su dependencia, sus puertos, sus volúmenes, redes... todo lo que hacíamos de forma manual y propenso a errores queda ahora más fácil de gestionar. Además, ahora ambos contenedores comparten el mismo archivo .env de variables de entorno.&lt;br /&gt;
&lt;br /&gt;
Bien, ahora vamos a levantar estos contenedores y acceder al contenedor web:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Inmediatamente después, ejecuta las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask db upgrade&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué ha ocurrido? Depende del tiempo que hayas tardado, puede que haya saltado un error o puede que haya funcionado. Puede que hayas intentado varias veces el comando anterior hasta que ha funcionado. ¿Por qué crees que ocurre esta aparente aleatoriedad?&lt;br /&gt;
&lt;br /&gt;
== Dependencias entre contenedores y entrypoints ==&lt;br /&gt;
&lt;br /&gt;
Fíjate que tenemos un &amp;quot;depends on&amp;quot; en el servicio web. Significa que el servicio &amp;quot;db&amp;quot; debe iniciarse antes que el servicio web. Pero, cuidado, ahí está la trampa, porque &amp;quot;servicio iniciado != servicio preparado&amp;quot;. El servicio de MariaDB tiene que arrancar, configurarse, etc...&lt;br /&gt;
&lt;br /&gt;
Cuando hicimos el comando COPY en nuestro Dockerfile, también copiamos la carpeta '''scripts'''. Uno de los scripts es '''wait-for-db.sh''', que realiza continuamente llamadas a MariaDB hasta que este servicio está listo para recibir peticiones. Ábrelo y estúdialo.&lt;br /&gt;
&lt;br /&gt;
Añade al servicio web de tu docker compose las siguientes líneas:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
entrypoint: [&amp;quot;/app/scripts/wait-for-db.sh&amp;quot;]&lt;br /&gt;
command: [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A su vez, del Dockerfile, hay que eliminar la línea de CMD que arrancaba nuestro servidor Flask.&lt;br /&gt;
&lt;br /&gt;
Luego, reinicializa todos los servicios:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. What? ¿No abre? Vamos a investigarlo, veamos el logs:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker logs web_app_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Nos debe salir un mensaje eterno de &amp;quot;/app/scripts/wait-for-db.sh: line 18: mariadb: command not found&amp;quot;. Esto ocurre porque en la imagen de nuestro servicio web NO TENEMOS el cliente de MariaDB. Debemos indicar entonces que lo instale. Añade justo debajo de FROM lo siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y --no-install-recommends mariadb-client &lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Al ser un contenedor basado en Linux, y accediendo como root, es como si actualizáramos todos los paquetes y luego instalásemos el cliente de MariaDB. Intentémoslo otra vez:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. ¡Jopelines! ¿Tampoco? Si haces un &amp;quot;docker ps&amp;quot; verás el problema. El contenedor web no tiene expuesto ningún puerto.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    (...)&lt;br /&gt;
    expose:&lt;br /&gt;
      - &amp;quot;5000&amp;quot;&lt;br /&gt;
    (...)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;quot;Expose&amp;quot; hace visible el puerto '''entre contenedores''', pero '''NO entre tu contenedor y el host'''. Para eso usamos la directiva &amp;quot;ports&amp;quot;, que se encarga del mapping. En nuestro caso, será:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  web:&lt;br /&gt;
    (...)&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;5000:5000&amp;quot;&lt;br /&gt;
    (...)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
En ocasiones, si los dos puertos coinciden, se puede dejar uno solo con &amp;quot;5000&amp;quot; pero dejaremos los dos. El de la izquierda siempre es el puerto externo (host), el de la derecha es el puerto interno (contenedor). Así que cambia ese detalle y por última vez:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml down&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Recuerda ejecutar las migraciones:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it web_app_container bash&lt;br /&gt;
flask db upgrade&lt;br /&gt;
exit&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que si falla, es porque la base aún no está lista, para eso tenemos el script... Y, ya que estamos, ¿se te ocurre alguna forma de automatizar este proceso? Es decir, que yo levante los contenedores y realice también las migraciones de la base de datos.&lt;br /&gt;
&lt;br /&gt;
Abre de nuevo [http://localhost:5000 http://localhost:5000]. ¡Ahora sí!&lt;br /&gt;
&lt;br /&gt;
== Trabajando con CMD, entrypoints y commands ==&lt;br /&gt;
&lt;br /&gt;
Piensa que al arrancar un contenedor Docker, siempre se ejecuta un único comando final.&lt;br /&gt;
Lo que hacen ENTRYPOINT, CMD y command (de Compose) es decidir qué comando será ese y con qué argumentos.&lt;br /&gt;
&lt;br /&gt;
=== CMD → “qué hacer por defecto” ===&lt;br /&gt;
&lt;br /&gt;
* Está en el Dockerfile.&lt;br /&gt;
* Es el comando que Docker ejecuta si no se indica otra cosa. En nuestro caso:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;, &amp;quot;--reload&amp;quot;, &amp;quot;--debug&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== ENTRYPOINT → “qué se ejecuta siempre” ===&lt;br /&gt;
&lt;br /&gt;
* También va en el Dockerfile, o se puede sobreescribir en el docker-compose.yml.&lt;br /&gt;
* Define el programa o script principal que siempre se ejecuta.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
ENTRYPOINT [&amp;quot;/app/scripts/wait-for-db.sh&amp;quot;]&lt;br /&gt;
CMD [&amp;quot;flask&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;--host=0.0.0.0&amp;quot;, &amp;quot;--port=5000&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Normalmente, el entrypoint se combina con CMD en modo &amp;quot;eh, ejecuta esto (entrypoint) y luego este comando final (cmd)&lt;br /&gt;
&lt;br /&gt;
Los entrypoints son muy útiles, sobre todo en los docker-compose's, porque puedo redefinir cómo quiero que inicie un contenedor en concreto '''SIN TENER QUE HACER UN BUILD DE NUEVO DE LA IMAGEN DE DOCKER'''. A diferencia de CMD, que viene predefinido, un entrypoint en mi docker-compose me da más versatilidad&lt;br /&gt;
&lt;br /&gt;
=== command (de docker-compose.yml) → “sobrescribe el CMD” ===&lt;br /&gt;
&lt;br /&gt;
En docker-compose.yml, el campo command: reemplaza al CMD del Dockerfile,pero no al ENTRYPOINT.&lt;br /&gt;
&lt;br /&gt;
== Combinación de elementos ==&lt;br /&gt;
&lt;br /&gt;
Es importante entender cómo Docker trabaja con la orquestación y con los contenedores:&lt;br /&gt;
&lt;br /&gt;
* Lee el YAML como un diccionario&lt;br /&gt;
* Ejecuta todo lo que haya en un Dockerfile EXCEPTUANDO directivas ENTRYPOINT y CMD&lt;br /&gt;
* Monta el volumen o los volúmenes de ese contenedor (si procede)&lt;br /&gt;
&lt;br /&gt;
Y ahora viene el detalle:&lt;br /&gt;
&lt;br /&gt;
* Si estamos levantando un contenedor a través de docker-compose, va a ejecutar los entrypoints y commands definidos en ese docker-compose.&lt;br /&gt;
* Un entrypoint definido en un docker-compose va a sustituir a un entrypoint definido en el Dockerfile&lt;br /&gt;
* Un command definido en un docker-compose va a sustituir a un CMD definido en el Dockerfile.&lt;br /&gt;
&lt;br /&gt;
Como consejo:&lt;br /&gt;
&lt;br /&gt;
* Definimos un entrypoint en el docker-compose. Esto es así porque está fuera de la imagen de Docker y nos permite redefinir el inicio del servicio sin modificar la imagen&lt;br /&gt;
* Eventualmente, definimos un CMD donde indicamos el comando final de ese contenedor al iniciar, que suele ser el arranque de Flask Server o de Gunicorn, dependiendo de si estamos en desarrollo o producción.&lt;br /&gt;
&lt;br /&gt;
No obstante, como nuestros entrypoints son todos scripts en bash, podemos definir en el mismo el arranque final (comando final) y nos olvidamos de CMD.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 5: Volumen de trabajo =&lt;br /&gt;
&lt;br /&gt;
== Problema de desarrollo ==&lt;br /&gt;
&lt;br /&gt;
Vale, ya parece que le hemos pillado el tranquillo a Docker y a Docker Compose. Ahora vamos a hacer algo como desarrolladores. Ve al archivo &amp;quot;app/templates/base_templates.html&amp;quot; y cambia la línea 22:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;title&amp;gt;{{ FLASK_APP_NAME }} - Repository of feature models in UVL &amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
por:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;title&amp;gt;Hello world! &amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. Fíjate en el nombre de la pestaña del navegador, ¿ha cambiado? ¿No? Tsss, esto de los contenedores... bueno, va, vamos a reiniciarlo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker restart web_app_container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. ¿Tampoco? Anda que vaya idea la de usar Docker... Claro, es a la hora de construir la imagen, hicimos &amp;quot;COPY . .&amp;quot;, pero no hemos reconstruido nada, así que...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml build --no-cache web&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --force-recreate web&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Importante señalar que le pedimos que ignore la caché para que fuerce a copiar de nuevo todo el contenido (incluyendo el cambio de línea) al interior del contenedor.&lt;br /&gt;
&lt;br /&gt;
Abre [http://localhost:5000 http://localhost:5000]. ¡Ahora sí! Pero... ¿tengo que reconstruir TODO el contenedor en cada cambio línea? ¿Y eso es trabajar de forma ágil?&lt;br /&gt;
&lt;br /&gt;
== Crear volumen de trabajo (bind mount) ==&lt;br /&gt;
&lt;br /&gt;
Evidentemente, eso no tiene sentido, tenemos que usar un volumen de trabajo que nos ayude a sincronizar el código de nuestro host con el código interno del contenedor. Luego en nuestro docker-compose:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  web:&lt;br /&gt;
    (....)&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;5000:5000&amp;quot;&lt;br /&gt;
    volumes:&lt;br /&gt;
      - ../:/app&lt;br /&gt;
    (....)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
La línea&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
- ../:/app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Monta tu carpeta local del proyecto (../) dentro del contenedor, (/app). ¿Y por qué /app? Porque fue el espacio de trabajo que definimos en el Dockerfile. Como ves, la congruencia entre los Dockerfile y el docker-compose es importante.&lt;br /&gt;
&lt;br /&gt;
Recarguemos de nuevo nustra configuración:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker compose -f docker/docker-compose.yml up -d --build&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ahora ya podemos trabajar con el código en host sin tener que reconstruir la imagen a cada cambio de línea.&lt;br /&gt;
&lt;br /&gt;
== Cuidado con COPY y los bind mounts ==&lt;br /&gt;
&lt;br /&gt;
Un '''bind mount''' (o “montaje de enlace”) es un tipo de volumen en el que Docker conecta directamente una carpeta de tu máquina (el host) con una carpeta dentro del contenedor.&lt;br /&gt;
&lt;br /&gt;
Cuando usas un bind mount (por ejemplo ../:/app), Docker monta tu carpeta local sobre la ruta /app del contenedor.&lt;br /&gt;
Eso significa que todo lo que estaba en /app dentro de la imagen desaparece al arrancar el contenedor, porque queda “oculto” por el volumen.&lt;br /&gt;
&lt;br /&gt;
Ahora bien, durante la construcción de la imagen usamos la directiva COPY. ¿Por qué? Porque necesitamos el archivo requirements.txt para instalar las dependencias de Python del proyecto. Pero... ¿realmente necesitamos copiar TODO? No, claro que no, podemos optar por copiar exclusivamente aquellos archivos que nos hagan falta durante '''LA CONSTRUCCIÓN''' de la imagen. El bind mount luego lo machacará.&lt;br /&gt;
&lt;br /&gt;
'''Muy importante:''' los bind mounts, al igual que cualquier otro volumen, están disponibles '''DESPUÉS''' de la construcción de la imagen. Es un error común creer que porque definamos un bind mount en un docker-compose, esos archivos aparecen ya para el contenedor. '''Esto es una fuente inagotable de problemas en la práctica'''.&lt;br /&gt;
&lt;br /&gt;
= Ejercicio 6: Distintas configuraciones de despliegue =&lt;br /&gt;
&lt;br /&gt;
Volvamos al punto de partida:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
rm -r docker&lt;br /&gt;
mv docker.bk docker&lt;br /&gt;
docker stop $(docker ps -a -q)&lt;br /&gt;
docker rm $(docker ps -a -q)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta práctica ha sido engorrosa a propósito para quitar esa &amp;quot;magia&amp;quot; que existe cuando lanzas un comando simple de orquestación y todo se levanta y funciona perfectamente. Esa situación está muy bien para desarrollar, pero no para aprender.&lt;br /&gt;
&lt;br /&gt;
Es importante que analices que existen distintos escenarios:&lt;br /&gt;
&lt;br /&gt;
* Hay un docker-compose para desarrollo.&lt;br /&gt;
* Hay 3 docker-compose para producción.&lt;br /&gt;
* Hay 6 Dockerfiles distintos&lt;br /&gt;
* Hay 3 entrypoints distintos&lt;br /&gt;
* Hay una carpeta llamada letsencrypt y otra llamada nginx. Dentro de nginx hay dos archivos de configuración dependiendo de si estamos en dev o en prod.&lt;br /&gt;
&lt;br /&gt;
== Autoevaluación ==&lt;br /&gt;
&lt;br /&gt;
Estudia bien cada archivo. Tómate tu tiempo e intenta responder a estas preguntas:&lt;br /&gt;
&lt;br /&gt;
* ¿Por qué en el docker-compose de dev NO se usa &amp;quot;ports&amp;quot; para enlazar con el host? (pista: nginx)&lt;br /&gt;
* ¿Qué es nginx y por qué es útil aquí?&lt;br /&gt;
* ¿Cuántos bind mounts usa el contenedor web en desarrollo? ¿Para qué sirve el bind mount del propio Docker?&lt;br /&gt;
* ¿Qué diferencia hay entre el entrypoint de desarrollo y el entrypoint de producción?&lt;br /&gt;
* ¿Por qué el servicio web en producción está montando todos esos bind mounts por separado? ¿No sería más sencillo hacer un volumen de todo el código?&lt;br /&gt;
&lt;br /&gt;
== Ejercicio propuesto ==&lt;br /&gt;
&lt;br /&gt;
* Levanta el entorno de Docker de desarrollo de uvlhub, siguendo la documentación oficial. Comprueba que puedes acceder a la app a través de localhost.&lt;br /&gt;
* Intenta correr los test de Selenium [https://docs.uvlhub.io/rosemary/testing/gui_tests#docker-environment-selenium-grid https://docs.uvlhub.io/rosemary/testing/gui_tests#docker-environment-selenium-grid]. Ten en cuenta que la forma de ejecutar y de visualizar los test de Selenium es distinta en un entorno Docker. Estudia bien la documentación, te hará falta para el desarrollo.&lt;br /&gt;
&lt;br /&gt;
== Comentarios finales ==&lt;br /&gt;
&lt;br /&gt;
=== Cuanto más se parezca el entorno de desarrollo al de producción, menos errores habrá ===&lt;br /&gt;
&lt;br /&gt;
En desarrollo es tentador usar configuraciones cómodas: servidor de Flask con --reload, bind mounts, base de datos vacía, logs en consola, etc.&lt;br /&gt;
Pero cuanto más difiere ese entorno de producción, más probable es que algo falle al desplegar.&lt;br /&gt;
&lt;br /&gt;
'''El código no falla por cambiar de servidor, sino porque los entornos se comportan distinto.'''&lt;br /&gt;
&lt;br /&gt;
Ejemplos clásicos:&lt;br /&gt;
&lt;br /&gt;
* En desarrollo usas SQLite, en producción MariaDB → aparecen errores de compatibilidad SQL.&lt;br /&gt;
* En desarrollo Flask sirve los archivos estáticos, en producción lo hace Nginx → las rutas cambian.&lt;br /&gt;
* En desarrollo ejecutas '''flask run''', en producción '''gunicorn''' → los hilos, workers o el reloader funcionan distinto.&lt;br /&gt;
&lt;br /&gt;
Por eso, el objetivo ideal es que tu entorno de desarrollo (dev), de preproducción (staging) y de producción (prod) sean idénticos salvo por las credenciales.&lt;br /&gt;
&lt;br /&gt;
Docker y docker-compose te permiten lograr eso: puedes usar la misma imagen base, cambiando solo el docker-compose (volúmenes, puertos, etc.).&lt;br /&gt;
&lt;br /&gt;
=== Diferencia entre el servidor de Flask y Gunicorn ===&lt;br /&gt;
&lt;br /&gt;
Flask trae incorporado un servidor de desarrollo.&lt;br /&gt;
Es muy útil para depurar porque:&lt;br /&gt;
&lt;br /&gt;
* recarga automáticamente el código (--reload),&lt;br /&gt;
* muestra errores detallados en el navegador,&lt;br /&gt;
* solo ejecuta un proceso.&lt;br /&gt;
&lt;br /&gt;
Sin embargo, no está pensado para producción:&lt;br /&gt;
&lt;br /&gt;
* no maneja múltiples conexiones concurrentes,&lt;br /&gt;
* no tiene control de procesos ni balanceo de carga,&lt;br /&gt;
* no implementa estándares de rendimiento o seguridad (como WSGI robusto).&lt;br /&gt;
&lt;br /&gt;
Ahí entra Gunicorn (Green Unicorn), que sí es un servidor WSGI  de producción (Web Server Gateway Interface, estándar de comunicación entre aplicaciones web Python y servidores web):&lt;br /&gt;
&lt;br /&gt;
* puede lanzar varios workers en paralelo,&lt;br /&gt;
* maneja múltiples peticiones concurrentes,&lt;br /&gt;
* se integra fácilmente con Nginx para servir contenido estático o HTTPS.&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Pr%C3%A1cticas_-_25/26&amp;diff=10166</id>
		<title>Prácticas - 25/26</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Pr%C3%A1cticas_-_25/26&amp;diff=10166"/>
				<updated>2025-10-14T06:18:28Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[[Página_Principal]] -&amp;gt; [[2025/2026]] -&amp;gt; [[Prácticas - 25/26]]&lt;br /&gt;
&lt;br /&gt;
== Prácticas ==&lt;br /&gt;
&lt;br /&gt;
* Práctica 1: '''Instalación del sistema base''' [[Archivo:EGC 2025-26 P1.pdf]]&lt;br /&gt;
* Práctica 2: '''Integración y despliegue continuos ''' [[Archivo:EGC 2025-26 P2.pdf]]&lt;br /&gt;
** [[Workflows solución P2 2526 |Workflows solución para los ejercicios propuestos]]&lt;br /&gt;
* Práctica 3: '''Gestión del código fuente''' [[Archivo:EGC_2025-26_P3.pdf]]&lt;br /&gt;
** Solución ejercicios alumno/a A: [[Archivo:P3 usuario A 2526.pdf]]&lt;br /&gt;
** Solución ejercicios alumno/a B: [[Archivo:P3 usuario B 2526.pdf]]&lt;br /&gt;
* Práctica 4: '''Automatización de pruebas''' [[Archivo:EGC_2025-26_P4.pdf]]&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
==== Material adicional para el desarrollo de las sesiones prácticas ====&lt;br /&gt;
&lt;br /&gt;
* Box con la imagen de Ubuntu de los equipos de laboratorio de prácticas : [https://portal.cloud.hashicorp.com/vagrant/discover/cdcetsii/UbuntuEGC_25]&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Archivo:EGC_2025-26_P4.pdf&amp;diff=10165</id>
		<title>Archivo:EGC 2025-26 P4.pdf</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Archivo:EGC_2025-26_P4.pdf&amp;diff=10165"/>
				<updated>2025-10-14T06:17:46Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10150</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10150"/>
				<updated>2025-10-10T08:21:25Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Parte 2: Creamos pruebas para nuestra aplicación UVLHUB */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de pruebas software en una aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
python3.12 -m venv .venv&lt;br /&gt;
source .venv/bin/activate&lt;br /&gt;
pip3.12 install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/routes.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas. En una aplicación real esta lista global sería reemplazada por una base de datos (como ocurre en UVLHUB). Usar una variable global hace que los datos se pierdan cada vez que se reinicia el servidor y pueda no funcionar correctamente si múltiples usuarios acceden a la vez. Sin embargo, para aprender a escribir pruebas, es una simplificación muy útil&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella desde primero desde el navegador (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;), creando y visualizando las tareas usando el formulario web. Y luego también interactúa con la app mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real. &lt;br /&gt;
&lt;br /&gt;
Por tanto, este archivo permite que las pruebas sean repetibles y aisladas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que se ha usado &amp;lt;code&amp;gt;sys.path.append&amp;lt;/code&amp;gt; por simplicidad, pero se considera una práctica un poco anticuada y frágil. Una alternativa más moderna y robusta, como has visto con Rosemary en prácticas anteriores, es convertir el proyecto en un paquete instalable. Para ello, se crearía un archivo &amp;lt;code&amp;gt;setup.py&amp;lt;/code&amp;gt; en la raíz del proyecto y luego se instalaría en modo editable con el comando &amp;lt;code&amp;gt;pip install -e .&amp;lt;/code&amp;gt;. De esta forma, pytest encontraría el paquete app automáticamente.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
Y, aunque esta práctica está centrada en la fase de automatización de pruebas, es importante que también reflexiones sobre la fase de diseño de pruebas. Por tanto, vuelve a echar un ojo a las pruebas y trata de identificar qué técnicas de diseño de las que hemos estudiado en la clase de teoría crees que se han seguido para su definición.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
Y, al igual que hiciste previamente, ¿puedes intuir qué técnicas de diseño de pruebas se han utilizado para su definición?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; mide el porcentaje de código ejecutado cuando se lanzan las pruebas para ayudarte a identificar las áreas de la aplicación que no están siendo probadas adecuadamente. Por tanto, ¿qué criterio de cobertura de los estudiados en clase de teoría usa &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
Name              Stmts   Miss  Cover&lt;br /&gt;
----------------  ------  ----  -----&lt;br /&gt;
app/__init__.py        1      0   100%&lt;br /&gt;
app/app.py             6      0   100%&lt;br /&gt;
app/models.py          9      0   100%&lt;br /&gt;
app/routes.py         26      2    92%&lt;br /&gt;
TOTAL                 42      2    95%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación a través de un navegador real.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
import os, time, pytest&lt;br /&gt;
from selenium import webdriver&lt;br /&gt;
from selenium.webdriver.common.by import By&lt;br /&gt;
from selenium.webdriver.firefox.service import Service&lt;br /&gt;
from webdriver_manager.firefox import GeckoDriverManager&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# === Configuración del navegador ===&lt;br /&gt;
&lt;br /&gt;
def initialize_driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Inicializa un driver de Firefox con configuración compatible con sistemas snap.&lt;br /&gt;
    UVLHUB usa exactamente esta estructura.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    options = webdriver.FirefoxOptions()&lt;br /&gt;
&lt;br /&gt;
    # Directorio temporal alternativo (evita problemas con permisos en snap)&lt;br /&gt;
    snap_tmp = os.path.expanduser(&amp;quot;~/snap/firefox/common/tmp&amp;quot;)&lt;br /&gt;
    os.makedirs(snap_tmp, exist_ok=True)&lt;br /&gt;
    os.environ[&amp;quot;TMPDIR&amp;quot;] = snap_tmp&lt;br /&gt;
&lt;br /&gt;
    service = Service(GeckoDriverManager().install())&lt;br /&gt;
    driver = webdriver.Firefox(service=service, options=options)&lt;br /&gt;
    driver.set_window_size(1024, 768)&lt;br /&gt;
    return driver&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def close_driver(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Cierra el navegador.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    driver.quit()&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# === Tests de interfaz ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
En relación al código utilizado, ten en cuenta que se ha usado &amp;lt;code&amp;gt;time.sleep(1)&amp;lt;/code&amp;gt; por simplicidad, pero es una práctica poco fiable. Una prueba puede fallar porque la red o el equipo van lentos y la página tarda más de 1 segundo en cargar. Si subes el tiempo (ej. &amp;lt;code&amp;gt;time.sleep(10)&amp;lt;/code&amp;gt;), haces las pruebas innecesariamente lentas. La solución es utilizar esperas explícitas, tal como se verás que se hace en UVLHUB, con las que se indica a Selenium que espere hasta que una condición se cumpla (por ejemplo, hasta que un elemento sea visible), con un tiempo máximo de espera.&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que en esta demostración usamos &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; para poder ver la actividad de los usuarios simulados en tiempo real en la consola. En una prueba de carga a gran escala se evitarían los &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; porque escribir en la consola consume recursos (I/O) y puede afectar ligeramente los resultados de rendimiento. Locust ya proporciona estadísticas detalladas en su interfaz web, que es la forma profesional de analizar los resultados, como vas a ver a continuación.&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura:&lt;br /&gt;
* el número de usuarios (por ejemplo, 10) &lt;br /&gt;
* la tasa de generación, es decir, cada cuánto tiempo se lanza un nuevo usuario (por ejemplo, 1)&lt;br /&gt;
* y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
En la terminal verás mensajes como estos hasta que se finalice la prueba:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, tanto en la clase de teoría como en esta práctica sobre automatización, ya tenemos las herramientas necesarias para poder diseñar e implementar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar una primera prueba para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía, comprobando el flujo completo desde el navegador del usuario. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas necesarias para comprobar automáticamente todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1), que son clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. Y es probable que hayas visto pruebas unitarias en las que se utilizan mocks (a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;) para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones reales, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa. &lt;br /&gt;
&lt;br /&gt;
Y aunque en esta práctica nos hemos centrado en técnicas de automatización, recuerda que &amp;quot;automatizar pruebas sin diseño es como hacer una pizza sin base&amp;quot; :-)&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10149</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10149"/>
				<updated>2025-10-10T08:16:45Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Pruebas de cobertura con pytest-cov */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de pruebas software en una aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
python3.12 -m venv .venv&lt;br /&gt;
source .venv/bin/activate&lt;br /&gt;
pip3.12 install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/routes.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas. En una aplicación real esta lista global sería reemplazada por una base de datos (como ocurre en UVLHUB). Usar una variable global hace que los datos se pierdan cada vez que se reinicia el servidor y pueda no funcionar correctamente si múltiples usuarios acceden a la vez. Sin embargo, para aprender a escribir pruebas, es una simplificación muy útil&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella desde primero desde el navegador (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;), creando y visualizando las tareas usando el formulario web. Y luego también interactúa con la app mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real. &lt;br /&gt;
&lt;br /&gt;
Por tanto, este archivo permite que las pruebas sean repetibles y aisladas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que se ha usado &amp;lt;code&amp;gt;sys.path.append&amp;lt;/code&amp;gt; por simplicidad, pero se considera una práctica un poco anticuada y frágil. Una alternativa más moderna y robusta, como has visto con Rosemary en prácticas anteriores, es convertir el proyecto en un paquete instalable. Para ello, se crearía un archivo &amp;lt;code&amp;gt;setup.py&amp;lt;/code&amp;gt; en la raíz del proyecto y luego se instalaría en modo editable con el comando &amp;lt;code&amp;gt;pip install -e .&amp;lt;/code&amp;gt;. De esta forma, pytest encontraría el paquete app automáticamente.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
Y, aunque esta práctica está centrada en la fase de automatización de pruebas, es importante que también reflexiones sobre la fase de diseño de pruebas. Por tanto, vuelve a echar un ojo a las pruebas y trata de identificar qué técnicas de diseño de las que hemos estudiado en la clase de teoría crees que se han seguido para su definición.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
Y, al igual que hiciste previamente, ¿puedes intuir qué técnicas de diseño de pruebas se han utilizado para su definición?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; mide el porcentaje de código ejecutado cuando se lanzan las pruebas para ayudarte a identificar las áreas de la aplicación que no están siendo probadas adecuadamente. Por tanto, ¿qué criterio de cobertura de los estudiados en clase de teoría usa &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
Name              Stmts   Miss  Cover&lt;br /&gt;
----------------  ------  ----  -----&lt;br /&gt;
app/__init__.py        1      0   100%&lt;br /&gt;
app/app.py             6      0   100%&lt;br /&gt;
app/models.py          9      0   100%&lt;br /&gt;
app/routes.py         26      2    92%&lt;br /&gt;
TOTAL                 42      2    95%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación a través de un navegador real.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
import os, time, pytest&lt;br /&gt;
from selenium import webdriver&lt;br /&gt;
from selenium.webdriver.common.by import By&lt;br /&gt;
from selenium.webdriver.firefox.service import Service&lt;br /&gt;
from webdriver_manager.firefox import GeckoDriverManager&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# === Configuración del navegador ===&lt;br /&gt;
&lt;br /&gt;
def initialize_driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Inicializa un driver de Firefox con configuración compatible con sistemas snap.&lt;br /&gt;
    UVLHUB usa exactamente esta estructura.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    options = webdriver.FirefoxOptions()&lt;br /&gt;
&lt;br /&gt;
    # Directorio temporal alternativo (evita problemas con permisos en snap)&lt;br /&gt;
    snap_tmp = os.path.expanduser(&amp;quot;~/snap/firefox/common/tmp&amp;quot;)&lt;br /&gt;
    os.makedirs(snap_tmp, exist_ok=True)&lt;br /&gt;
    os.environ[&amp;quot;TMPDIR&amp;quot;] = snap_tmp&lt;br /&gt;
&lt;br /&gt;
    service = Service(GeckoDriverManager().install())&lt;br /&gt;
    driver = webdriver.Firefox(service=service, options=options)&lt;br /&gt;
    driver.set_window_size(1024, 768)&lt;br /&gt;
    return driver&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def close_driver(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Cierra el navegador.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    driver.quit()&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# === Tests de interfaz ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
En relación al código utilizado, ten en cuenta que se ha usado &amp;lt;code&amp;gt;time.sleep(1)&amp;lt;/code&amp;gt; por simplicidad, pero es una práctica poco fiable. Una prueba puede fallar porque la red o el equipo van lentos y la página tarda más de 1 segundo en cargar. Si subes el tiempo (ej. &amp;lt;code&amp;gt;time.sleep(10)&amp;lt;/code&amp;gt;), haces las pruebas innecesariamente lentas. La solución es utilizar esperas explícitas, tal como se verás que se hace en UVLHUB, con las que se indica a Selenium que espere hasta que una condición se cumpla (por ejemplo, hasta que un elemento sea visible), con un tiempo máximo de espera.&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que en esta demostración usamos &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; para poder ver la actividad de los usuarios simulados en tiempo real en la consola. En una prueba de carga a gran escala se evitarían los &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; porque escribir en la consola consume recursos (I/O) y puede afectar ligeramente los resultados de rendimiento. Locust ya proporciona estadísticas detalladas en su interfaz web, que es la forma profesional de analizar los resultados, como vas a ver a continuación.&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura:&lt;br /&gt;
* el número de usuarios (por ejemplo, 10) &lt;br /&gt;
* la tasa de generación, es decir, cada cuánto tiempo se lanza un nuevo usuario (por ejemplo, 1)&lt;br /&gt;
* y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
En la terminal verás mensajes como estos hasta que se finalice la prueba:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar una primera prueba para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía, comprobando el flujo completo desde el navegador del usuario. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1), que son clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. Y es probable que hayas visto pruebas unitarias en las que se utilizan mocks (a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;) para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones reales, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10148</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10148"/>
				<updated>2025-10-10T08:15:02Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Pruebas de integración con pytest */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de pruebas software en una aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
python3.12 -m venv .venv&lt;br /&gt;
source .venv/bin/activate&lt;br /&gt;
pip3.12 install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/routes.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas. En una aplicación real esta lista global sería reemplazada por una base de datos (como ocurre en UVLHUB). Usar una variable global hace que los datos se pierdan cada vez que se reinicia el servidor y pueda no funcionar correctamente si múltiples usuarios acceden a la vez. Sin embargo, para aprender a escribir pruebas, es una simplificación muy útil&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella desde primero desde el navegador (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;), creando y visualizando las tareas usando el formulario web. Y luego también interactúa con la app mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real. &lt;br /&gt;
&lt;br /&gt;
Por tanto, este archivo permite que las pruebas sean repetibles y aisladas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que se ha usado &amp;lt;code&amp;gt;sys.path.append&amp;lt;/code&amp;gt; por simplicidad, pero se considera una práctica un poco anticuada y frágil. Una alternativa más moderna y robusta, como has visto con Rosemary en prácticas anteriores, es convertir el proyecto en un paquete instalable. Para ello, se crearía un archivo &amp;lt;code&amp;gt;setup.py&amp;lt;/code&amp;gt; en la raíz del proyecto y luego se instalaría en modo editable con el comando &amp;lt;code&amp;gt;pip install -e .&amp;lt;/code&amp;gt;. De esta forma, pytest encontraría el paquete app automáticamente.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
Y, aunque esta práctica está centrada en la fase de automatización de pruebas, es importante que también reflexiones sobre la fase de diseño de pruebas. Por tanto, vuelve a echar un ojo a las pruebas y trata de identificar qué técnicas de diseño de las que hemos estudiado en la clase de teoría crees que se han seguido para su definición.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
Y, al igual que hiciste previamente, ¿puedes intuir qué técnicas de diseño de pruebas se han utilizado para su definición?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
Name              Stmts   Miss  Cover&lt;br /&gt;
----------------  ------  ----  -----&lt;br /&gt;
app/__init__.py        1      0   100%&lt;br /&gt;
app/app.py             6      0   100%&lt;br /&gt;
app/models.py          9      0   100%&lt;br /&gt;
app/routes.py         26      2    92%&lt;br /&gt;
TOTAL                 42      2    95%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación a través de un navegador real.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
import os, time, pytest&lt;br /&gt;
from selenium import webdriver&lt;br /&gt;
from selenium.webdriver.common.by import By&lt;br /&gt;
from selenium.webdriver.firefox.service import Service&lt;br /&gt;
from webdriver_manager.firefox import GeckoDriverManager&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# === Configuración del navegador ===&lt;br /&gt;
&lt;br /&gt;
def initialize_driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Inicializa un driver de Firefox con configuración compatible con sistemas snap.&lt;br /&gt;
    UVLHUB usa exactamente esta estructura.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    options = webdriver.FirefoxOptions()&lt;br /&gt;
&lt;br /&gt;
    # Directorio temporal alternativo (evita problemas con permisos en snap)&lt;br /&gt;
    snap_tmp = os.path.expanduser(&amp;quot;~/snap/firefox/common/tmp&amp;quot;)&lt;br /&gt;
    os.makedirs(snap_tmp, exist_ok=True)&lt;br /&gt;
    os.environ[&amp;quot;TMPDIR&amp;quot;] = snap_tmp&lt;br /&gt;
&lt;br /&gt;
    service = Service(GeckoDriverManager().install())&lt;br /&gt;
    driver = webdriver.Firefox(service=service, options=options)&lt;br /&gt;
    driver.set_window_size(1024, 768)&lt;br /&gt;
    return driver&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def close_driver(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Cierra el navegador.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    driver.quit()&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# === Tests de interfaz ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
En relación al código utilizado, ten en cuenta que se ha usado &amp;lt;code&amp;gt;time.sleep(1)&amp;lt;/code&amp;gt; por simplicidad, pero es una práctica poco fiable. Una prueba puede fallar porque la red o el equipo van lentos y la página tarda más de 1 segundo en cargar. Si subes el tiempo (ej. &amp;lt;code&amp;gt;time.sleep(10)&amp;lt;/code&amp;gt;), haces las pruebas innecesariamente lentas. La solución es utilizar esperas explícitas, tal como se verás que se hace en UVLHUB, con las que se indica a Selenium que espere hasta que una condición se cumpla (por ejemplo, hasta que un elemento sea visible), con un tiempo máximo de espera.&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que en esta demostración usamos &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; para poder ver la actividad de los usuarios simulados en tiempo real en la consola. En una prueba de carga a gran escala se evitarían los &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; porque escribir en la consola consume recursos (I/O) y puede afectar ligeramente los resultados de rendimiento. Locust ya proporciona estadísticas detalladas en su interfaz web, que es la forma profesional de analizar los resultados, como vas a ver a continuación.&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura:&lt;br /&gt;
* el número de usuarios (por ejemplo, 10) &lt;br /&gt;
* la tasa de generación, es decir, cada cuánto tiempo se lanza un nuevo usuario (por ejemplo, 1)&lt;br /&gt;
* y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
En la terminal verás mensajes como estos hasta que se finalice la prueba:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar una primera prueba para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía, comprobando el flujo completo desde el navegador del usuario. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1), que son clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. Y es probable que hayas visto pruebas unitarias en las que se utilizan mocks (a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;) para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones reales, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10147</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10147"/>
				<updated>2025-10-10T08:13:30Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Pruebas unitarias con pytest */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de pruebas software en una aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
python3.12 -m venv .venv&lt;br /&gt;
source .venv/bin/activate&lt;br /&gt;
pip3.12 install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/routes.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas. En una aplicación real esta lista global sería reemplazada por una base de datos (como ocurre en UVLHUB). Usar una variable global hace que los datos se pierdan cada vez que se reinicia el servidor y pueda no funcionar correctamente si múltiples usuarios acceden a la vez. Sin embargo, para aprender a escribir pruebas, es una simplificación muy útil&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella desde primero desde el navegador (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;), creando y visualizando las tareas usando el formulario web. Y luego también interactúa con la app mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real. &lt;br /&gt;
&lt;br /&gt;
Por tanto, este archivo permite que las pruebas sean repetibles y aisladas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que se ha usado &amp;lt;code&amp;gt;sys.path.append&amp;lt;/code&amp;gt; por simplicidad, pero se considera una práctica un poco anticuada y frágil. Una alternativa más moderna y robusta, como has visto con Rosemary en prácticas anteriores, es convertir el proyecto en un paquete instalable. Para ello, se crearía un archivo &amp;lt;code&amp;gt;setup.py&amp;lt;/code&amp;gt; en la raíz del proyecto y luego se instalaría en modo editable con el comando &amp;lt;code&amp;gt;pip install -e .&amp;lt;/code&amp;gt;. De esta forma, pytest encontraría el paquete app automáticamente.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
Y, aunque esta práctica está centrada en la fase de automatización de pruebas, es importante que también reflexiones sobre la fase de diseño de pruebas. Por tanto, vuelve a echar un ojo a las pruebas y trata de identificar qué técnicas de diseño de las que hemos estudiado en la clase de teoría crees que se han seguido para su definición.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
Name              Stmts   Miss  Cover&lt;br /&gt;
----------------  ------  ----  -----&lt;br /&gt;
app/__init__.py        1      0   100%&lt;br /&gt;
app/app.py             6      0   100%&lt;br /&gt;
app/models.py          9      0   100%&lt;br /&gt;
app/routes.py         26      2    92%&lt;br /&gt;
TOTAL                 42      2    95%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación a través de un navegador real.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
import os, time, pytest&lt;br /&gt;
from selenium import webdriver&lt;br /&gt;
from selenium.webdriver.common.by import By&lt;br /&gt;
from selenium.webdriver.firefox.service import Service&lt;br /&gt;
from webdriver_manager.firefox import GeckoDriverManager&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# === Configuración del navegador ===&lt;br /&gt;
&lt;br /&gt;
def initialize_driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Inicializa un driver de Firefox con configuración compatible con sistemas snap.&lt;br /&gt;
    UVLHUB usa exactamente esta estructura.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    options = webdriver.FirefoxOptions()&lt;br /&gt;
&lt;br /&gt;
    # Directorio temporal alternativo (evita problemas con permisos en snap)&lt;br /&gt;
    snap_tmp = os.path.expanduser(&amp;quot;~/snap/firefox/common/tmp&amp;quot;)&lt;br /&gt;
    os.makedirs(snap_tmp, exist_ok=True)&lt;br /&gt;
    os.environ[&amp;quot;TMPDIR&amp;quot;] = snap_tmp&lt;br /&gt;
&lt;br /&gt;
    service = Service(GeckoDriverManager().install())&lt;br /&gt;
    driver = webdriver.Firefox(service=service, options=options)&lt;br /&gt;
    driver.set_window_size(1024, 768)&lt;br /&gt;
    return driver&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def close_driver(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Cierra el navegador.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    driver.quit()&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# === Tests de interfaz ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
En relación al código utilizado, ten en cuenta que se ha usado &amp;lt;code&amp;gt;time.sleep(1)&amp;lt;/code&amp;gt; por simplicidad, pero es una práctica poco fiable. Una prueba puede fallar porque la red o el equipo van lentos y la página tarda más de 1 segundo en cargar. Si subes el tiempo (ej. &amp;lt;code&amp;gt;time.sleep(10)&amp;lt;/code&amp;gt;), haces las pruebas innecesariamente lentas. La solución es utilizar esperas explícitas, tal como se verás que se hace en UVLHUB, con las que se indica a Selenium que espere hasta que una condición se cumpla (por ejemplo, hasta que un elemento sea visible), con un tiempo máximo de espera.&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que en esta demostración usamos &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; para poder ver la actividad de los usuarios simulados en tiempo real en la consola. En una prueba de carga a gran escala se evitarían los &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; porque escribir en la consola consume recursos (I/O) y puede afectar ligeramente los resultados de rendimiento. Locust ya proporciona estadísticas detalladas en su interfaz web, que es la forma profesional de analizar los resultados, como vas a ver a continuación.&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura:&lt;br /&gt;
* el número de usuarios (por ejemplo, 10) &lt;br /&gt;
* la tasa de generación, es decir, cada cuánto tiempo se lanza un nuevo usuario (por ejemplo, 1)&lt;br /&gt;
* y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
En la terminal verás mensajes como estos hasta que se finalice la prueba:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar una primera prueba para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía, comprobando el flujo completo desde el navegador del usuario. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1), que son clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. Y es probable que hayas visto pruebas unitarias en las que se utilizan mocks (a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;) para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones reales, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10141</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10141"/>
				<updated>2025-10-07T09:49:07Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Pruebas de interfaz con Selenium */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de pruebas software en una aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
python3.12 -m venv .venv&lt;br /&gt;
source .venv/bin/activate&lt;br /&gt;
pip3.12 install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/routes.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas. En una aplicación real esta lista global sería reemplazada por una base de datos (como ocurre en UVLHUB). Usar una variable global hace que los datos se pierdan cada vez que se reinicia el servidor y pueda no funcionar correctamente si múltiples usuarios acceden a la vez. Sin embargo, para aprender a escribir pruebas, es una simplificación muy útil&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella desde primero desde el navegador (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;), creando y visualizando las tareas usando el formulario web. Y luego también interactúa con la app mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real. &lt;br /&gt;
&lt;br /&gt;
Por tanto, este archivo permite que las pruebas sean repetibles y aisladas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que se ha usado &amp;lt;code&amp;gt;sys.path.append&amp;lt;/code&amp;gt; por simplicidad, pero se considera una práctica un poco anticuada y frágil. Una alternativa más moderna y robusta, como has visto con Rosemary en prácticas anteriores, es convertir el proyecto en un paquete instalable. Para ello, se crearía un archivo &amp;lt;code&amp;gt;setup.py&amp;lt;/code&amp;gt; en la raíz del proyecto y luego se instalaría en modo editable con el comando &amp;lt;code&amp;gt;pip install -e .&amp;lt;/code&amp;gt;. De esta forma, pytest encontraría el paquete app automáticamente.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
Name              Stmts   Miss  Cover&lt;br /&gt;
----------------  ------  ----  -----&lt;br /&gt;
app/__init__.py        1      0   100%&lt;br /&gt;
app/app.py             6      0   100%&lt;br /&gt;
app/models.py          9      0   100%&lt;br /&gt;
app/routes.py         26      2    92%&lt;br /&gt;
TOTAL                 42      2    95%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación a través de un navegador real.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
import os, time, pytest&lt;br /&gt;
from selenium import webdriver&lt;br /&gt;
from selenium.webdriver.common.by import By&lt;br /&gt;
from selenium.webdriver.firefox.service import Service&lt;br /&gt;
from webdriver_manager.firefox import GeckoDriverManager&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# === Configuración del navegador ===&lt;br /&gt;
&lt;br /&gt;
def initialize_driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Inicializa un driver de Firefox con configuración compatible con sistemas snap.&lt;br /&gt;
    UVLHUB usa exactamente esta estructura.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    options = webdriver.FirefoxOptions()&lt;br /&gt;
&lt;br /&gt;
    # Directorio temporal alternativo (evita problemas con permisos en snap)&lt;br /&gt;
    snap_tmp = os.path.expanduser(&amp;quot;~/snap/firefox/common/tmp&amp;quot;)&lt;br /&gt;
    os.makedirs(snap_tmp, exist_ok=True)&lt;br /&gt;
    os.environ[&amp;quot;TMPDIR&amp;quot;] = snap_tmp&lt;br /&gt;
&lt;br /&gt;
    service = Service(GeckoDriverManager().install())&lt;br /&gt;
    driver = webdriver.Firefox(service=service, options=options)&lt;br /&gt;
    driver.set_window_size(1024, 768)&lt;br /&gt;
    return driver&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def close_driver(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Cierra el navegador.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    driver.quit()&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# === Tests de interfaz ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
En relación al código utilizado, ten en cuenta que se ha usado &amp;lt;code&amp;gt;time.sleep(1)&amp;lt;/code&amp;gt; por simplicidad, pero es una práctica poco fiable. Una prueba puede fallar porque la red o el equipo van lentos y la página tarda más de 1 segundo en cargar. Si subes el tiempo (ej. &amp;lt;code&amp;gt;time.sleep(10)&amp;lt;/code&amp;gt;), haces las pruebas innecesariamente lentas. La solución es utilizar esperas explícitas, tal como se verás que se hace en UVLHUB, con las que se indica a Selenium que espere hasta que una condición se cumpla (por ejemplo, hasta que un elemento sea visible), con un tiempo máximo de espera.&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que en esta demostración usamos &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; para poder ver la actividad de los usuarios simulados en tiempo real en la consola. En una prueba de carga a gran escala se evitarían los &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; porque escribir en la consola consume recursos (I/O) y puede afectar ligeramente los resultados de rendimiento. Locust ya proporciona estadísticas detalladas en su interfaz web, que es la forma profesional de analizar los resultados, como vas a ver a continuación.&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura:&lt;br /&gt;
* el número de usuarios (por ejemplo, 10) &lt;br /&gt;
* la tasa de generación, es decir, cada cuánto tiempo se lanza un nuevo usuario (por ejemplo, 1)&lt;br /&gt;
* y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
En la terminal verás mensajes como estos hasta que se finalice la prueba:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar una primera prueba para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía, comprobando el flujo completo desde el navegador del usuario. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1), que son clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. Y es probable que hayas visto pruebas unitarias en las que se utilizan mocks (a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;) para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones reales, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10140</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10140"/>
				<updated>2025-10-07T09:46:58Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Configuración del entorno de pruebas con conftest.py */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de pruebas software en una aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
python3.12 -m venv .venv&lt;br /&gt;
source .venv/bin/activate&lt;br /&gt;
pip3.12 install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/routes.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas. En una aplicación real esta lista global sería reemplazada por una base de datos (como ocurre en UVLHUB). Usar una variable global hace que los datos se pierdan cada vez que se reinicia el servidor y pueda no funcionar correctamente si múltiples usuarios acceden a la vez. Sin embargo, para aprender a escribir pruebas, es una simplificación muy útil&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella desde primero desde el navegador (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;), creando y visualizando las tareas usando el formulario web. Y luego también interactúa con la app mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real. &lt;br /&gt;
&lt;br /&gt;
Por tanto, este archivo permite que las pruebas sean repetibles y aisladas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que se ha usado &amp;lt;code&amp;gt;sys.path.append&amp;lt;/code&amp;gt; por simplicidad, pero se considera una práctica un poco anticuada y frágil. Una alternativa más moderna y robusta, como has visto con Rosemary en prácticas anteriores, es convertir el proyecto en un paquete instalable. Para ello, se crearía un archivo &amp;lt;code&amp;gt;setup.py&amp;lt;/code&amp;gt; en la raíz del proyecto y luego se instalaría en modo editable con el comando &amp;lt;code&amp;gt;pip install -e .&amp;lt;/code&amp;gt;. De esta forma, pytest encontraría el paquete app automáticamente.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
Name              Stmts   Miss  Cover&lt;br /&gt;
----------------  ------  ----  -----&lt;br /&gt;
app/__init__.py        1      0   100%&lt;br /&gt;
app/app.py             6      0   100%&lt;br /&gt;
app/models.py          9      0   100%&lt;br /&gt;
app/routes.py         26      2    92%&lt;br /&gt;
TOTAL                 42      2    95%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación a través de un navegador real.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
import os, time, pytest&lt;br /&gt;
from selenium import webdriver&lt;br /&gt;
from selenium.webdriver.common.by import By&lt;br /&gt;
from selenium.webdriver.firefox.service import Service&lt;br /&gt;
from webdriver_manager.firefox import GeckoDriverManager&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# === Configuración del navegador ===&lt;br /&gt;
&lt;br /&gt;
def initialize_driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Inicializa un driver de Firefox con configuración compatible con sistemas snap.&lt;br /&gt;
    UVLHUB usa exactamente esta estructura.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    options = webdriver.FirefoxOptions()&lt;br /&gt;
&lt;br /&gt;
    # Directorio temporal alternativo (evita problemas con permisos en snap)&lt;br /&gt;
    snap_tmp = os.path.expanduser(&amp;quot;~/snap/firefox/common/tmp&amp;quot;)&lt;br /&gt;
    os.makedirs(snap_tmp, exist_ok=True)&lt;br /&gt;
    os.environ[&amp;quot;TMPDIR&amp;quot;] = snap_tmp&lt;br /&gt;
&lt;br /&gt;
    service = Service(GeckoDriverManager().install())&lt;br /&gt;
    driver = webdriver.Firefox(service=service, options=options)&lt;br /&gt;
    driver.set_window_size(1024, 768)&lt;br /&gt;
    return driver&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def close_driver(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Cierra el navegador.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    driver.quit()&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# === Tests de interfaz ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
En relación al código utilizado, ten en cuenta que se ha usado &amp;lt;code&amp;gt;time.sleep(1)&amp;lt;/code&amp;gt; por simplicidad, pero es una práctica poco fiable. Una prueba puede fallar porque la red o el equipo van lentos y la página tarda más de 1 segundo en cargar. Si subes el tiempo (ej. &amp;lt;code&amp;gt;time.sleep(10)&amp;lt;/code&amp;gt;), haces las pruebas innecesariamente lentas. La solución es utilizar esperas explícitas (explicit waits), tal como se verás que se hace en UVLHUB, con las que se indica a Selenium que espere hasta que una condición se cumpla (por ejemplo, hasta que un elemento sea visible), con un tiempo máximo de espera.&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que en esta demostración usamos &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; para poder ver la actividad de los usuarios simulados en tiempo real en la consola. En una prueba de carga a gran escala se evitarían los &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; porque escribir en la consola consume recursos (I/O) y puede afectar ligeramente los resultados de rendimiento. Locust ya proporciona estadísticas detalladas en su interfaz web, que es la forma profesional de analizar los resultados, como vas a ver a continuación.&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura:&lt;br /&gt;
* el número de usuarios (por ejemplo, 10) &lt;br /&gt;
* la tasa de generación, es decir, cada cuánto tiempo se lanza un nuevo usuario (por ejemplo, 1)&lt;br /&gt;
* y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
En la terminal verás mensajes como estos hasta que se finalice la prueba:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar una primera prueba para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía, comprobando el flujo completo desde el navegador del usuario. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1), que son clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. Y es probable que hayas visto pruebas unitarias en las que se utilizan mocks (a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;) para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones reales, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10138</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10138"/>
				<updated>2025-10-07T09:28:46Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Algunas cuestiones que puedes investigar */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de pruebas software en una aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
python3.12 -m venv .venv&lt;br /&gt;
source .venv/bin/activate&lt;br /&gt;
pip3.12 install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/routes.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas. En una aplicación real esta lista global sería reemplazada por una base de datos (como ocurre en UVLHUB). Usar una variable global hace que los datos se pierdan cada vez que se reinicia el servidor y pueda no funcionar correctamente si múltiples usuarios acceden a la vez. Sin embargo, para aprender a escribir pruebas, es una simplificación muy útil&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella desde primero desde el navegador (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;), creando y visualizando las tareas usando el formulario web. Y luego también interactúa con la app mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real. &lt;br /&gt;
&lt;br /&gt;
Por tanto, este archivo permite que las pruebas sean repetibles y aisladas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que se ha usado &amp;lt;code&amp;gt;sys.path.append&amp;lt;/code&amp;gt; por simplicidad, pero se considera una práctica un poco anticuada y frágil. Una alternativa más moderna y robusta, como has visto con Rosemary en UVLHUB, es convertir el proyecto en un paquete instalable. Para ello, se crearía un archivo &amp;lt;code&amp;gt;setup.py&amp;lt;/code&amp;gt; en la raíz del proyecto y luego se instalaría en modo editable con el comando &amp;lt;code&amp;gt;pip install -e .&amp;lt;/code&amp;gt;. De esta forma, pytest encontraría el paquete app automáticamente.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
Name              Stmts   Miss  Cover&lt;br /&gt;
----------------  ------  ----  -----&lt;br /&gt;
app/__init__.py        1      0   100%&lt;br /&gt;
app/app.py             6      0   100%&lt;br /&gt;
app/models.py          9      0   100%&lt;br /&gt;
app/routes.py         26      2    92%&lt;br /&gt;
TOTAL                 42      2    95%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación a través de un navegador real.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
import os, time, pytest&lt;br /&gt;
from selenium import webdriver&lt;br /&gt;
from selenium.webdriver.common.by import By&lt;br /&gt;
from selenium.webdriver.firefox.service import Service&lt;br /&gt;
from webdriver_manager.firefox import GeckoDriverManager&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# === Configuración del navegador ===&lt;br /&gt;
&lt;br /&gt;
def initialize_driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Inicializa un driver de Firefox con configuración compatible con sistemas snap.&lt;br /&gt;
    UVLHUB usa exactamente esta estructura.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    options = webdriver.FirefoxOptions()&lt;br /&gt;
&lt;br /&gt;
    # Directorio temporal alternativo (evita problemas con permisos en snap)&lt;br /&gt;
    snap_tmp = os.path.expanduser(&amp;quot;~/snap/firefox/common/tmp&amp;quot;)&lt;br /&gt;
    os.makedirs(snap_tmp, exist_ok=True)&lt;br /&gt;
    os.environ[&amp;quot;TMPDIR&amp;quot;] = snap_tmp&lt;br /&gt;
&lt;br /&gt;
    service = Service(GeckoDriverManager().install())&lt;br /&gt;
    driver = webdriver.Firefox(service=service, options=options)&lt;br /&gt;
    driver.set_window_size(1024, 768)&lt;br /&gt;
    return driver&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def close_driver(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Cierra el navegador.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    driver.quit()&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# === Tests de interfaz ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
En relación al código utilizado, ten en cuenta que se ha usado &amp;lt;code&amp;gt;time.sleep(1)&amp;lt;/code&amp;gt; por simplicidad, pero es una práctica poco fiable. Una prueba puede fallar porque la red o el equipo van lentos y la página tarda más de 1 segundo en cargar. Si subes el tiempo (ej. &amp;lt;code&amp;gt;time.sleep(10)&amp;lt;/code&amp;gt;), haces las pruebas innecesariamente lentas. La solución es utilizar esperas explícitas (explicit waits), tal como se verás que se hace en UVLHUB, con las que se indica a Selenium que espere hasta que una condición se cumpla (por ejemplo, hasta que un elemento sea visible), con un tiempo máximo de espera.&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que en esta demostración usamos &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; para poder ver la actividad de los usuarios simulados en tiempo real en la consola. En una prueba de carga a gran escala se evitarían los &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; porque escribir en la consola consume recursos (I/O) y puede afectar ligeramente los resultados de rendimiento. Locust ya proporciona estadísticas detalladas en su interfaz web, que es la forma profesional de analizar los resultados, como vas a ver a continuación.&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura:&lt;br /&gt;
* el número de usuarios (por ejemplo, 10) &lt;br /&gt;
* la tasa de generación, es decir, cada cuánto tiempo se lanza un nuevo usuario (por ejemplo, 1)&lt;br /&gt;
* y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
En la terminal verás mensajes como estos hasta que se finalice la prueba:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar una primera prueba para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía, comprobando el flujo completo desde el navegador del usuario. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1), que son clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. Y es probable que hayas visto pruebas unitarias en las que se utilizan mocks (a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;) para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones reales, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10137</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10137"/>
				<updated>2025-10-07T09:26:56Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Ejecución de Locust */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de pruebas software en una aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
python3.12 -m venv .venv&lt;br /&gt;
source .venv/bin/activate&lt;br /&gt;
pip3.12 install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/routes.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas. En una aplicación real esta lista global sería reemplazada por una base de datos (como ocurre en UVLHUB). Usar una variable global hace que los datos se pierdan cada vez que se reinicia el servidor y pueda no funcionar correctamente si múltiples usuarios acceden a la vez. Sin embargo, para aprender a escribir pruebas, es una simplificación muy útil&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella desde primero desde el navegador (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;), creando y visualizando las tareas usando el formulario web. Y luego también interactúa con la app mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real. &lt;br /&gt;
&lt;br /&gt;
Por tanto, este archivo permite que las pruebas sean repetibles y aisladas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que se ha usado &amp;lt;code&amp;gt;sys.path.append&amp;lt;/code&amp;gt; por simplicidad, pero se considera una práctica un poco anticuada y frágil. Una alternativa más moderna y robusta, como has visto con Rosemary en UVLHUB, es convertir el proyecto en un paquete instalable. Para ello, se crearía un archivo &amp;lt;code&amp;gt;setup.py&amp;lt;/code&amp;gt; en la raíz del proyecto y luego se instalaría en modo editable con el comando &amp;lt;code&amp;gt;pip install -e .&amp;lt;/code&amp;gt;. De esta forma, pytest encontraría el paquete app automáticamente.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
Name              Stmts   Miss  Cover&lt;br /&gt;
----------------  ------  ----  -----&lt;br /&gt;
app/__init__.py        1      0   100%&lt;br /&gt;
app/app.py             6      0   100%&lt;br /&gt;
app/models.py          9      0   100%&lt;br /&gt;
app/routes.py         26      2    92%&lt;br /&gt;
TOTAL                 42      2    95%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación a través de un navegador real.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
import os, time, pytest&lt;br /&gt;
from selenium import webdriver&lt;br /&gt;
from selenium.webdriver.common.by import By&lt;br /&gt;
from selenium.webdriver.firefox.service import Service&lt;br /&gt;
from webdriver_manager.firefox import GeckoDriverManager&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# === Configuración del navegador ===&lt;br /&gt;
&lt;br /&gt;
def initialize_driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Inicializa un driver de Firefox con configuración compatible con sistemas snap.&lt;br /&gt;
    UVLHUB usa exactamente esta estructura.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    options = webdriver.FirefoxOptions()&lt;br /&gt;
&lt;br /&gt;
    # Directorio temporal alternativo (evita problemas con permisos en snap)&lt;br /&gt;
    snap_tmp = os.path.expanduser(&amp;quot;~/snap/firefox/common/tmp&amp;quot;)&lt;br /&gt;
    os.makedirs(snap_tmp, exist_ok=True)&lt;br /&gt;
    os.environ[&amp;quot;TMPDIR&amp;quot;] = snap_tmp&lt;br /&gt;
&lt;br /&gt;
    service = Service(GeckoDriverManager().install())&lt;br /&gt;
    driver = webdriver.Firefox(service=service, options=options)&lt;br /&gt;
    driver.set_window_size(1024, 768)&lt;br /&gt;
    return driver&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def close_driver(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Cierra el navegador.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    driver.quit()&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# === Tests de interfaz ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
En relación al código utilizado, ten en cuenta que se ha usado &amp;lt;code&amp;gt;time.sleep(1)&amp;lt;/code&amp;gt; por simplicidad, pero es una práctica poco fiable. Una prueba puede fallar porque la red o el equipo van lentos y la página tarda más de 1 segundo en cargar. Si subes el tiempo (ej. &amp;lt;code&amp;gt;time.sleep(10)&amp;lt;/code&amp;gt;), haces las pruebas innecesariamente lentas. La solución es utilizar esperas explícitas (explicit waits), tal como se verás que se hace en UVLHUB, con las que se indica a Selenium que espere hasta que una condición se cumpla (por ejemplo, hasta que un elemento sea visible), con un tiempo máximo de espera.&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que en esta demostración usamos &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; para poder ver la actividad de los usuarios simulados en tiempo real en la consola. En una prueba de carga a gran escala se evitarían los &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; porque escribir en la consola consume recursos (I/O) y puede afectar ligeramente los resultados de rendimiento. Locust ya proporciona estadísticas detalladas en su interfaz web, que es la forma profesional de analizar los resultados, como vas a ver a continuación.&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura:&lt;br /&gt;
* el número de usuarios (por ejemplo, 10) &lt;br /&gt;
* la tasa de generación, es decir, cada cuánto tiempo se lanza un nuevo usuario (por ejemplo, 1)&lt;br /&gt;
* y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
En la terminal verás mensajes como estos hasta que se finalice la prueba:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar una primera prueba para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía, comprobando el flujo completo desde el navegador del usuario. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1). Estas clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. Es probable que hayas visto pruebas unitarias en las que se utilizan mocks (a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;) para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones reales, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10136</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10136"/>
				<updated>2025-10-07T09:25:09Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Ejecución de Locust */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de pruebas software en una aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
python3.12 -m venv .venv&lt;br /&gt;
source .venv/bin/activate&lt;br /&gt;
pip3.12 install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/routes.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas. En una aplicación real esta lista global sería reemplazada por una base de datos (como ocurre en UVLHUB). Usar una variable global hace que los datos se pierdan cada vez que se reinicia el servidor y pueda no funcionar correctamente si múltiples usuarios acceden a la vez. Sin embargo, para aprender a escribir pruebas, es una simplificación muy útil&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella desde primero desde el navegador (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;), creando y visualizando las tareas usando el formulario web. Y luego también interactúa con la app mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real. &lt;br /&gt;
&lt;br /&gt;
Por tanto, este archivo permite que las pruebas sean repetibles y aisladas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que se ha usado &amp;lt;code&amp;gt;sys.path.append&amp;lt;/code&amp;gt; por simplicidad, pero se considera una práctica un poco anticuada y frágil. Una alternativa más moderna y robusta, como has visto con Rosemary en UVLHUB, es convertir el proyecto en un paquete instalable. Para ello, se crearía un archivo &amp;lt;code&amp;gt;setup.py&amp;lt;/code&amp;gt; en la raíz del proyecto y luego se instalaría en modo editable con el comando &amp;lt;code&amp;gt;pip install -e .&amp;lt;/code&amp;gt;. De esta forma, pytest encontraría el paquete app automáticamente.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
Name              Stmts   Miss  Cover&lt;br /&gt;
----------------  ------  ----  -----&lt;br /&gt;
app/__init__.py        1      0   100%&lt;br /&gt;
app/app.py             6      0   100%&lt;br /&gt;
app/models.py          9      0   100%&lt;br /&gt;
app/routes.py         26      2    92%&lt;br /&gt;
TOTAL                 42      2    95%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación a través de un navegador real.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
import os, time, pytest&lt;br /&gt;
from selenium import webdriver&lt;br /&gt;
from selenium.webdriver.common.by import By&lt;br /&gt;
from selenium.webdriver.firefox.service import Service&lt;br /&gt;
from webdriver_manager.firefox import GeckoDriverManager&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# === Configuración del navegador ===&lt;br /&gt;
&lt;br /&gt;
def initialize_driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Inicializa un driver de Firefox con configuración compatible con sistemas snap.&lt;br /&gt;
    UVLHUB usa exactamente esta estructura.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    options = webdriver.FirefoxOptions()&lt;br /&gt;
&lt;br /&gt;
    # Directorio temporal alternativo (evita problemas con permisos en snap)&lt;br /&gt;
    snap_tmp = os.path.expanduser(&amp;quot;~/snap/firefox/common/tmp&amp;quot;)&lt;br /&gt;
    os.makedirs(snap_tmp, exist_ok=True)&lt;br /&gt;
    os.environ[&amp;quot;TMPDIR&amp;quot;] = snap_tmp&lt;br /&gt;
&lt;br /&gt;
    service = Service(GeckoDriverManager().install())&lt;br /&gt;
    driver = webdriver.Firefox(service=service, options=options)&lt;br /&gt;
    driver.set_window_size(1024, 768)&lt;br /&gt;
    return driver&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def close_driver(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Cierra el navegador.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    driver.quit()&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# === Tests de interfaz ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
En relación al código utilizado, ten en cuenta que se ha usado &amp;lt;code&amp;gt;time.sleep(1)&amp;lt;/code&amp;gt; por simplicidad, pero es una práctica poco fiable. Una prueba puede fallar porque la red o el equipo van lentos y la página tarda más de 1 segundo en cargar. Si subes el tiempo (ej. &amp;lt;code&amp;gt;time.sleep(10)&amp;lt;/code&amp;gt;), haces las pruebas innecesariamente lentas. La solución es utilizar esperas explícitas (explicit waits), tal como se verás que se hace en UVLHUB, con las que se indica a Selenium que espere hasta que una condición se cumpla (por ejemplo, hasta que un elemento sea visible), con un tiempo máximo de espera.&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que en esta demostración usamos &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; para poder ver la actividad de los usuarios simulados en tiempo real en la consola. En una prueba de carga a gran escala se evitarían los &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; porque escribir en la consola consume recursos (I/O) y puede afectar ligeramente los resultados de rendimiento. Locust ya proporciona estadísticas detalladas en su interfaz web, que es la forma profesional de analizar los resultados, como vas a ver a continuación.&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura:&lt;br /&gt;
* el número de usuarios (por ejemplo, 10) &lt;br /&gt;
* la tasa de generación, es decir, cada cuánto tiempo se lanza un nuevo usuario (por ejemplo, 1)&lt;br /&gt;
* y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {&amp;quot;WebsiteTestUser&amp;quot;: 10} (10 total users)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar una primera prueba para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía, comprobando el flujo completo desde el navegador del usuario. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1). Estas clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. Es probable que hayas visto pruebas unitarias en las que se utilizan mocks (a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;) para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones reales, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10135</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10135"/>
				<updated>2025-10-07T09:24:48Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Ejecución de Locust */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de pruebas software en una aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
python3.12 -m venv .venv&lt;br /&gt;
source .venv/bin/activate&lt;br /&gt;
pip3.12 install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/routes.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas. En una aplicación real esta lista global sería reemplazada por una base de datos (como ocurre en UVLHUB). Usar una variable global hace que los datos se pierdan cada vez que se reinicia el servidor y pueda no funcionar correctamente si múltiples usuarios acceden a la vez. Sin embargo, para aprender a escribir pruebas, es una simplificación muy útil&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella desde primero desde el navegador (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;), creando y visualizando las tareas usando el formulario web. Y luego también interactúa con la app mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real. &lt;br /&gt;
&lt;br /&gt;
Por tanto, este archivo permite que las pruebas sean repetibles y aisladas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que se ha usado &amp;lt;code&amp;gt;sys.path.append&amp;lt;/code&amp;gt; por simplicidad, pero se considera una práctica un poco anticuada y frágil. Una alternativa más moderna y robusta, como has visto con Rosemary en UVLHUB, es convertir el proyecto en un paquete instalable. Para ello, se crearía un archivo &amp;lt;code&amp;gt;setup.py&amp;lt;/code&amp;gt; en la raíz del proyecto y luego se instalaría en modo editable con el comando &amp;lt;code&amp;gt;pip install -e .&amp;lt;/code&amp;gt;. De esta forma, pytest encontraría el paquete app automáticamente.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
Name              Stmts   Miss  Cover&lt;br /&gt;
----------------  ------  ----  -----&lt;br /&gt;
app/__init__.py        1      0   100%&lt;br /&gt;
app/app.py             6      0   100%&lt;br /&gt;
app/models.py          9      0   100%&lt;br /&gt;
app/routes.py         26      2    92%&lt;br /&gt;
TOTAL                 42      2    95%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación a través de un navegador real.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
import os, time, pytest&lt;br /&gt;
from selenium import webdriver&lt;br /&gt;
from selenium.webdriver.common.by import By&lt;br /&gt;
from selenium.webdriver.firefox.service import Service&lt;br /&gt;
from webdriver_manager.firefox import GeckoDriverManager&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# === Configuración del navegador ===&lt;br /&gt;
&lt;br /&gt;
def initialize_driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Inicializa un driver de Firefox con configuración compatible con sistemas snap.&lt;br /&gt;
    UVLHUB usa exactamente esta estructura.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    options = webdriver.FirefoxOptions()&lt;br /&gt;
&lt;br /&gt;
    # Directorio temporal alternativo (evita problemas con permisos en snap)&lt;br /&gt;
    snap_tmp = os.path.expanduser(&amp;quot;~/snap/firefox/common/tmp&amp;quot;)&lt;br /&gt;
    os.makedirs(snap_tmp, exist_ok=True)&lt;br /&gt;
    os.environ[&amp;quot;TMPDIR&amp;quot;] = snap_tmp&lt;br /&gt;
&lt;br /&gt;
    service = Service(GeckoDriverManager().install())&lt;br /&gt;
    driver = webdriver.Firefox(service=service, options=options)&lt;br /&gt;
    driver.set_window_size(1024, 768)&lt;br /&gt;
    return driver&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def close_driver(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Cierra el navegador.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    driver.quit()&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# === Tests de interfaz ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
En relación al código utilizado, ten en cuenta que se ha usado &amp;lt;code&amp;gt;time.sleep(1)&amp;lt;/code&amp;gt; por simplicidad, pero es una práctica poco fiable. Una prueba puede fallar porque la red o el equipo van lentos y la página tarda más de 1 segundo en cargar. Si subes el tiempo (ej. &amp;lt;code&amp;gt;time.sleep(10)&amp;lt;/code&amp;gt;), haces las pruebas innecesariamente lentas. La solución es utilizar esperas explícitas (explicit waits), tal como se verás que se hace en UVLHUB, con las que se indica a Selenium que espere hasta que una condición se cumpla (por ejemplo, hasta que un elemento sea visible), con un tiempo máximo de espera.&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que en esta demostración usamos &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; para poder ver la actividad de los usuarios simulados en tiempo real en la consola. En una prueba de carga a gran escala se evitarían los &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; porque escribir en la consola consume recursos (I/O) y puede afectar ligeramente los resultados de rendimiento. Locust ya proporciona estadísticas detalladas en su interfaz web, que es la forma profesional de analizar los resultados, como vas a ver a continuación.&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura:&lt;br /&gt;
# el número de usuarios (por ejemplo, 10) &lt;br /&gt;
# la tasa de generación, es decir, cada cuánto tiempo se lanza un nuevo usuario (por ejemplo, 1)&lt;br /&gt;
# y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {&amp;quot;WebsiteTestUser&amp;quot;: 10} (10 total users)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar una primera prueba para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía, comprobando el flujo completo desde el navegador del usuario. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1). Estas clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. Es probable que hayas visto pruebas unitarias en las que se utilizan mocks (a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;) para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones reales, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10134</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10134"/>
				<updated>2025-10-07T09:22:24Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Ejecución de Locust */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de pruebas software en una aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
python3.12 -m venv .venv&lt;br /&gt;
source .venv/bin/activate&lt;br /&gt;
pip3.12 install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/routes.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas. En una aplicación real esta lista global sería reemplazada por una base de datos (como ocurre en UVLHUB). Usar una variable global hace que los datos se pierdan cada vez que se reinicia el servidor y pueda no funcionar correctamente si múltiples usuarios acceden a la vez. Sin embargo, para aprender a escribir pruebas, es una simplificación muy útil&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella desde primero desde el navegador (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;), creando y visualizando las tareas usando el formulario web. Y luego también interactúa con la app mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real. &lt;br /&gt;
&lt;br /&gt;
Por tanto, este archivo permite que las pruebas sean repetibles y aisladas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que se ha usado &amp;lt;code&amp;gt;sys.path.append&amp;lt;/code&amp;gt; por simplicidad, pero se considera una práctica un poco anticuada y frágil. Una alternativa más moderna y robusta, como has visto con Rosemary en UVLHUB, es convertir el proyecto en un paquete instalable. Para ello, se crearía un archivo &amp;lt;code&amp;gt;setup.py&amp;lt;/code&amp;gt; en la raíz del proyecto y luego se instalaría en modo editable con el comando &amp;lt;code&amp;gt;pip install -e .&amp;lt;/code&amp;gt;. De esta forma, pytest encontraría el paquete app automáticamente.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
Name              Stmts   Miss  Cover&lt;br /&gt;
----------------  ------  ----  -----&lt;br /&gt;
app/__init__.py        1      0   100%&lt;br /&gt;
app/app.py             6      0   100%&lt;br /&gt;
app/models.py          9      0   100%&lt;br /&gt;
app/routes.py         26      2    92%&lt;br /&gt;
TOTAL                 42      2    95%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación a través de un navegador real.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
import os, time, pytest&lt;br /&gt;
from selenium import webdriver&lt;br /&gt;
from selenium.webdriver.common.by import By&lt;br /&gt;
from selenium.webdriver.firefox.service import Service&lt;br /&gt;
from webdriver_manager.firefox import GeckoDriverManager&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# === Configuración del navegador ===&lt;br /&gt;
&lt;br /&gt;
def initialize_driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Inicializa un driver de Firefox con configuración compatible con sistemas snap.&lt;br /&gt;
    UVLHUB usa exactamente esta estructura.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    options = webdriver.FirefoxOptions()&lt;br /&gt;
&lt;br /&gt;
    # Directorio temporal alternativo (evita problemas con permisos en snap)&lt;br /&gt;
    snap_tmp = os.path.expanduser(&amp;quot;~/snap/firefox/common/tmp&amp;quot;)&lt;br /&gt;
    os.makedirs(snap_tmp, exist_ok=True)&lt;br /&gt;
    os.environ[&amp;quot;TMPDIR&amp;quot;] = snap_tmp&lt;br /&gt;
&lt;br /&gt;
    service = Service(GeckoDriverManager().install())&lt;br /&gt;
    driver = webdriver.Firefox(service=service, options=options)&lt;br /&gt;
    driver.set_window_size(1024, 768)&lt;br /&gt;
    return driver&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def close_driver(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Cierra el navegador.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    driver.quit()&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# === Tests de interfaz ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
En relación al código utilizado, ten en cuenta que se ha usado &amp;lt;code&amp;gt;time.sleep(1)&amp;lt;/code&amp;gt; por simplicidad, pero es una práctica poco fiable. Una prueba puede fallar porque la red o el equipo van lentos y la página tarda más de 1 segundo en cargar. Si subes el tiempo (ej. &amp;lt;code&amp;gt;time.sleep(10)&amp;lt;/code&amp;gt;), haces las pruebas innecesariamente lentas. La solución es utilizar esperas explícitas (explicit waits), tal como se verás que se hace en UVLHUB, con las que se indica a Selenium que espere hasta que una condición se cumpla (por ejemplo, hasta que un elemento sea visible), con un tiempo máximo de espera.&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que en esta demostración usamos &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; para poder ver la actividad de los usuarios simulados en tiempo real en la consola. En una prueba de carga a gran escala se evitarían los &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; porque escribir en la consola consume recursos (I/O) y puede afectar ligeramente los resultados de rendimiento. Locust ya proporciona estadísticas detalladas en su interfaz web, que es la forma profesional de analizar los resultados, como vas a ver a continuación.&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
# Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura el número de usuarios -por ejemplo, 10-, la tasa de generación (cada cuánto tiempo se lanza un nuevo usuario)-por ejemplo, 1- y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;). Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
# En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {&amp;quot;WebsiteTestUser&amp;quot;: 10} (10 total users)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar una primera prueba para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía, comprobando el flujo completo desde el navegador del usuario. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1). Estas clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. Es probable que hayas visto pruebas unitarias en las que se utilizan mocks (a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;) para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones reales, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10133</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10133"/>
				<updated>2025-10-07T09:21:24Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Pruebas de interfaz con Selenium */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de pruebas software en una aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
python3.12 -m venv .venv&lt;br /&gt;
source .venv/bin/activate&lt;br /&gt;
pip3.12 install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/routes.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas. En una aplicación real esta lista global sería reemplazada por una base de datos (como ocurre en UVLHUB). Usar una variable global hace que los datos se pierdan cada vez que se reinicia el servidor y pueda no funcionar correctamente si múltiples usuarios acceden a la vez. Sin embargo, para aprender a escribir pruebas, es una simplificación muy útil&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella desde primero desde el navegador (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;), creando y visualizando las tareas usando el formulario web. Y luego también interactúa con la app mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real. &lt;br /&gt;
&lt;br /&gt;
Por tanto, este archivo permite que las pruebas sean repetibles y aisladas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que se ha usado &amp;lt;code&amp;gt;sys.path.append&amp;lt;/code&amp;gt; por simplicidad, pero se considera una práctica un poco anticuada y frágil. Una alternativa más moderna y robusta, como has visto con Rosemary en UVLHUB, es convertir el proyecto en un paquete instalable. Para ello, se crearía un archivo &amp;lt;code&amp;gt;setup.py&amp;lt;/code&amp;gt; en la raíz del proyecto y luego se instalaría en modo editable con el comando &amp;lt;code&amp;gt;pip install -e .&amp;lt;/code&amp;gt;. De esta forma, pytest encontraría el paquete app automáticamente.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
Name              Stmts   Miss  Cover&lt;br /&gt;
----------------  ------  ----  -----&lt;br /&gt;
app/__init__.py        1      0   100%&lt;br /&gt;
app/app.py             6      0   100%&lt;br /&gt;
app/models.py          9      0   100%&lt;br /&gt;
app/routes.py         26      2    92%&lt;br /&gt;
TOTAL                 42      2    95%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación a través de un navegador real.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
import os, time, pytest&lt;br /&gt;
from selenium import webdriver&lt;br /&gt;
from selenium.webdriver.common.by import By&lt;br /&gt;
from selenium.webdriver.firefox.service import Service&lt;br /&gt;
from webdriver_manager.firefox import GeckoDriverManager&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# === Configuración del navegador ===&lt;br /&gt;
&lt;br /&gt;
def initialize_driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Inicializa un driver de Firefox con configuración compatible con sistemas snap.&lt;br /&gt;
    UVLHUB usa exactamente esta estructura.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    options = webdriver.FirefoxOptions()&lt;br /&gt;
&lt;br /&gt;
    # Directorio temporal alternativo (evita problemas con permisos en snap)&lt;br /&gt;
    snap_tmp = os.path.expanduser(&amp;quot;~/snap/firefox/common/tmp&amp;quot;)&lt;br /&gt;
    os.makedirs(snap_tmp, exist_ok=True)&lt;br /&gt;
    os.environ[&amp;quot;TMPDIR&amp;quot;] = snap_tmp&lt;br /&gt;
&lt;br /&gt;
    service = Service(GeckoDriverManager().install())&lt;br /&gt;
    driver = webdriver.Firefox(service=service, options=options)&lt;br /&gt;
    driver.set_window_size(1024, 768)&lt;br /&gt;
    return driver&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def close_driver(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Cierra el navegador.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    driver.quit()&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# === Tests de interfaz ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
En relación al código utilizado, ten en cuenta que se ha usado &amp;lt;code&amp;gt;time.sleep(1)&amp;lt;/code&amp;gt; por simplicidad, pero es una práctica poco fiable. Una prueba puede fallar porque la red o el equipo van lentos y la página tarda más de 1 segundo en cargar. Si subes el tiempo (ej. &amp;lt;code&amp;gt;time.sleep(10)&amp;lt;/code&amp;gt;), haces las pruebas innecesariamente lentas. La solución es utilizar esperas explícitas (explicit waits), tal como se verás que se hace en UVLHUB, con las que se indica a Selenium que espere hasta que una condición se cumpla (por ejemplo, hasta que un elemento sea visible), con un tiempo máximo de espera.&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que en esta demostración usamos &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; para poder ver la actividad de los usuarios simulados en tiempo real en la consola. En una prueba de carga a gran escala se evitarían los &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; porque escribir en la consola consume recursos (I/O) y puede afectar ligeramente los resultados de rendimiento. Locust ya proporciona estadísticas detalladas en su interfaz web, que es la forma profesional de analizar los resultados, como vas a ver a continuación.&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
# Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura el número de usuarios -por ejemplo, 10-, la tasa de generación (cada cuánto tiempo se lanza un nuevo usuario)-por ejemplo, 1- y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;). Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
# En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {&amp;quot;WebsiteTestUser&amp;quot;: 10} (10 total users)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar una primera prueba para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía, comprobando el flujo completo desde el navegador del usuario. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1). Estas clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. Es probable que hayas visto pruebas unitarias en las que se utilizan mocks (a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;) para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones reales, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10132</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10132"/>
				<updated>2025-10-07T09:20:00Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Exportar el test a código Selenium WebDriver */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de pruebas software en una aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
python3.12 -m venv .venv&lt;br /&gt;
source .venv/bin/activate&lt;br /&gt;
pip3.12 install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/routes.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas. En una aplicación real esta lista global sería reemplazada por una base de datos (como ocurre en UVLHUB). Usar una variable global hace que los datos se pierdan cada vez que se reinicia el servidor y pueda no funcionar correctamente si múltiples usuarios acceden a la vez. Sin embargo, para aprender a escribir pruebas, es una simplificación muy útil&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella desde primero desde el navegador (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;), creando y visualizando las tareas usando el formulario web. Y luego también interactúa con la app mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real. &lt;br /&gt;
&lt;br /&gt;
Por tanto, este archivo permite que las pruebas sean repetibles y aisladas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que se ha usado &amp;lt;code&amp;gt;sys.path.append&amp;lt;/code&amp;gt; por simplicidad, pero se considera una práctica un poco anticuada y frágil. Una alternativa más moderna y robusta, como has visto con Rosemary en UVLHUB, es convertir el proyecto en un paquete instalable. Para ello, se crearía un archivo &amp;lt;code&amp;gt;setup.py&amp;lt;/code&amp;gt; en la raíz del proyecto y luego se instalaría en modo editable con el comando &amp;lt;code&amp;gt;pip install -e .&amp;lt;/code&amp;gt;. De esta forma, pytest encontraría el paquete app automáticamente.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
Name              Stmts   Miss  Cover&lt;br /&gt;
----------------  ------  ----  -----&lt;br /&gt;
app/__init__.py        1      0   100%&lt;br /&gt;
app/app.py             6      0   100%&lt;br /&gt;
app/models.py          9      0   100%&lt;br /&gt;
app/routes.py         26      2    92%&lt;br /&gt;
TOTAL                 42      2    95%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación a través de un navegador real.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
import os, time, pytest&lt;br /&gt;
from selenium import webdriver&lt;br /&gt;
from selenium.webdriver.common.by import By&lt;br /&gt;
from selenium.webdriver.firefox.service import Service&lt;br /&gt;
from webdriver_manager.firefox import GeckoDriverManager&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# === Configuración del navegador (idéntica al estilo UVLHUB) ===&lt;br /&gt;
&lt;br /&gt;
def initialize_driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Inicializa un driver de Firefox con configuración compatible con sistemas snap.&lt;br /&gt;
    UVLHUB usa exactamente esta estructura.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    options = webdriver.FirefoxOptions()&lt;br /&gt;
&lt;br /&gt;
    # Directorio temporal alternativo (evita problemas con permisos en snap)&lt;br /&gt;
    snap_tmp = os.path.expanduser(&amp;quot;~/snap/firefox/common/tmp&amp;quot;)&lt;br /&gt;
    os.makedirs(snap_tmp, exist_ok=True)&lt;br /&gt;
    os.environ[&amp;quot;TMPDIR&amp;quot;] = snap_tmp&lt;br /&gt;
&lt;br /&gt;
    service = Service(GeckoDriverManager().install())&lt;br /&gt;
    driver = webdriver.Firefox(service=service, options=options)&lt;br /&gt;
    driver.set_window_size(1024, 768)&lt;br /&gt;
    return driver&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def close_driver(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Cierra el navegador.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    driver.quit()&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# === Tests de interfaz ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
En relación al código utilizado, ten en cuenta que se ha usado &amp;lt;code&amp;gt;time.sleep(1)&amp;lt;/code&amp;gt; por simplicidad, pero es una práctica poco fiable. Una prueba puede fallar porque la red o el equipo van lentos y la página tarda más de 1 segundo en cargar. Si subes el tiempo (ej. &amp;lt;code&amp;gt;time.sleep(10)&amp;lt;/code&amp;gt;), haces las pruebas innecesariamente lentas. La solución es utilizar esperas explícitas (explicit waits), tal como se verás que se hace en UVLHUB, con las que se indica a Selenium que espere hasta que una condición se cumpla (por ejemplo, hasta que un elemento sea visible), con un tiempo máximo de espera.&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que en esta demostración usamos &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; para poder ver la actividad de los usuarios simulados en tiempo real en la consola. En una prueba de carga a gran escala se evitarían los &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; porque escribir en la consola consume recursos (I/O) y puede afectar ligeramente los resultados de rendimiento. Locust ya proporciona estadísticas detalladas en su interfaz web, que es la forma profesional de analizar los resultados, como vas a ver a continuación.&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
# Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura el número de usuarios -por ejemplo, 10-, la tasa de generación (cada cuánto tiempo se lanza un nuevo usuario)-por ejemplo, 1- y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;). Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
# En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {&amp;quot;WebsiteTestUser&amp;quot;: 10} (10 total users)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar una primera prueba para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía, comprobando el flujo completo desde el navegador del usuario. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1). Estas clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. Es probable que hayas visto pruebas unitarias en las que se utilizan mocks (a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;) para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones reales, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10128</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10128"/>
				<updated>2025-10-07T08:57:48Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Pruebas de interfaz con Selenium */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de pruebas software en una aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
python3.12 -m venv .venv&lt;br /&gt;
source .venv/bin/activate&lt;br /&gt;
pip3.12 install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/routes.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas. En una aplicación real esta lista global sería reemplazada por una base de datos (como ocurre en UVLHUB). Usar una variable global hace que los datos se pierdan cada vez que se reinicia el servidor y pueda no funcionar correctamente si múltiples usuarios acceden a la vez. Sin embargo, para aprender a escribir pruebas, es una simplificación muy útil&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella desde primero desde el navegador (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;), creando y visualizando las tareas usando el formulario web. Y luego también interactúa con la app mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real. &lt;br /&gt;
&lt;br /&gt;
Por tanto, este archivo permite que las pruebas sean repetibles y aisladas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que se ha usado &amp;lt;code&amp;gt;sys.path.append&amp;lt;/code&amp;gt; por simplicidad, pero se considera una práctica un poco anticuada y frágil. Una alternativa más moderna y robusta, como has visto con Rosemary en UVLHUB, es convertir el proyecto en un paquete instalable. Para ello, se crearía un archivo &amp;lt;code&amp;gt;setup.py&amp;lt;/code&amp;gt; en la raíz del proyecto y luego se instalaría en modo editable con el comando &amp;lt;code&amp;gt;pip install -e .&amp;lt;/code&amp;gt;. De esta forma, pytest encontraría el paquete app automáticamente.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
Name              Stmts   Miss  Cover&lt;br /&gt;
----------------  ------  ----  -----&lt;br /&gt;
app/__init__.py        1      0   100%&lt;br /&gt;
app/app.py             6      0   100%&lt;br /&gt;
app/models.py          9      0   100%&lt;br /&gt;
app/routes.py         26      2    92%&lt;br /&gt;
TOTAL                 42      2    95%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación a través de un navegador real.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
import os, time, pytest&lt;br /&gt;
from selenium import webdriver&lt;br /&gt;
from selenium.webdriver.common.by import By&lt;br /&gt;
from selenium.webdriver.firefox.service import Service&lt;br /&gt;
from webdriver_manager.firefox import GeckoDriverManager&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# === Configuración del navegador (idéntica al estilo UVLHUB) ===&lt;br /&gt;
&lt;br /&gt;
def initialize_driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Inicializa un driver de Firefox con configuración compatible con sistemas snap.&lt;br /&gt;
    UVLHUB usa exactamente esta estructura.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    options = webdriver.FirefoxOptions()&lt;br /&gt;
&lt;br /&gt;
    # Directorio temporal alternativo (evita problemas con permisos en snap)&lt;br /&gt;
    snap_tmp = os.path.expanduser(&amp;quot;~/snap/firefox/common/tmp&amp;quot;)&lt;br /&gt;
    os.makedirs(snap_tmp, exist_ok=True)&lt;br /&gt;
    os.environ[&amp;quot;TMPDIR&amp;quot;] = snap_tmp&lt;br /&gt;
&lt;br /&gt;
    service = Service(GeckoDriverManager().install())&lt;br /&gt;
    driver = webdriver.Firefox(service=service, options=options)&lt;br /&gt;
    driver.set_window_size(1024, 768)&lt;br /&gt;
    return driver&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def close_driver(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Cierra el navegador.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    driver.quit()&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# === Tests de interfaz ===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
En relación al código utilizado, ten en cuenta que se ha usado &amp;lt;code&amp;gt;time.sleep(1)&amp;lt;/code&amp;gt; por simplicidad, pero es una práctica poco fiable. Una prueba puede fallar porque la red o el equipo van lentos y la página tarda más de 1 segundo en cargar. Si subes el tiempo (ej. &amp;lt;code&amp;gt;time.sleep(10)&amp;lt;/code&amp;gt;), haces las pruebas innecesariamente lentas. La solución es utilizar esperas explícitas (explicit waits), tal como se verás que se hace en UVLHUB, con las que se indica a Selenium que espere hasta que una condición se cumpla (por ejemplo, hasta que un elemento sea visible), con un tiempo máximo de espera.&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
    &lt;br /&gt;
Ejecutar el test exportado:&lt;br /&gt;
&lt;br /&gt;
Y ya puedes ejecutar el test exportado utilizando pytest:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest tests/test_selenium_ide.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto ejecutará el test generado por &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en tu navegador usando &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que en esta demostración usamos &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; para poder ver la actividad de los usuarios simulados en tiempo real en la consola. En una prueba de carga a gran escala se evitarían los &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; porque escribir en la consola consume recursos (I/O) y puede afectar ligeramente los resultados de rendimiento. Locust ya proporciona estadísticas detalladas en su interfaz web, que es la forma profesional de analizar los resultados, como vas a ver a continuación.&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
# Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura el número de usuarios -por ejemplo, 10-, la tasa de generación (cada cuánto tiempo se lanza un nuevo usuario)-por ejemplo, 1- y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;). Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
# En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {&amp;quot;WebsiteTestUser&amp;quot;: 10} (10 total users)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar una primera prueba para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía, comprobando el flujo completo desde el navegador del usuario. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1). Estas clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. Es probable que hayas visto pruebas unitarias en las que se utilizan mocks (a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;) para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones reales, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10124</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10124"/>
				<updated>2025-10-07T08:47:22Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Ejecuta la aplicación */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de pruebas software en una aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
python3.12 -m venv .venv&lt;br /&gt;
source .venv/bin/activate&lt;br /&gt;
pip3.12 install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/routes.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas. En una aplicación real esta lista global sería reemplazada por una base de datos (como ocurre en UVLHUB). Usar una variable global hace que los datos se pierdan cada vez que se reinicia el servidor y pueda no funcionar correctamente si múltiples usuarios acceden a la vez. Sin embargo, para aprender a escribir pruebas, es una simplificación muy útil&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella desde primero desde el navegador (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;), creando y visualizando las tareas usando el formulario web. Y luego también interactúa con la app mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real. &lt;br /&gt;
&lt;br /&gt;
Por tanto, este archivo permite que las pruebas sean repetibles y aisladas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que se ha usado &amp;lt;code&amp;gt;sys.path.append&amp;lt;/code&amp;gt; por simplicidad, pero se considera una práctica un poco anticuada y frágil. Una alternativa más moderna y robusta, como has visto con Rosemary en UVLHUB, es convertir el proyecto en un paquete instalable. Para ello, se crearía un archivo &amp;lt;code&amp;gt;setup.py&amp;lt;/code&amp;gt; en la raíz del proyecto y luego se instalaría en modo editable con el comando &amp;lt;code&amp;gt;pip install -e .&amp;lt;/code&amp;gt;. De esta forma, pytest encontraría el paquete app automáticamente.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
Name              Stmts   Miss  Cover&lt;br /&gt;
----------------  ------  ----  -----&lt;br /&gt;
app/__init__.py        1      0   100%&lt;br /&gt;
app/app.py             6      0   100%&lt;br /&gt;
app/models.py          9      0   100%&lt;br /&gt;
app/routes.py         26      2    92%&lt;br /&gt;
TOTAL                 42      2    95%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
En relación al código utilizado, ten en cuenta que se ha usado &amp;lt;code&amp;gt;time.sleep(1)&amp;lt;/code&amp;gt; por simplicidad, pero es una práctica poco fiable. Una prueba puede fallar porque la red o el equipo van lentos y la página tarda más de 1 segundo en cargar. Si subes el tiempo (ej. &amp;lt;code&amp;gt;time.sleep(10)&amp;lt;/code&amp;gt;), haces las pruebas innecesariamente lentas. La solución es utilizar esperas explícitas (explicit waits), tal como se verás que se hace en UVLHUB, con las que se indica a Selenium que espere hasta que una condición se cumpla (por ejemplo, hasta que un elemento sea visible), con un tiempo máximo de espera.&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
    &lt;br /&gt;
Ejecutar el test exportado:&lt;br /&gt;
&lt;br /&gt;
Y ya puedes ejecutar el test exportado utilizando pytest:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest tests/test_selenium_ide.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto ejecutará el test generado por &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en tu navegador usando &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que en esta demostración usamos &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; para poder ver la actividad de los usuarios simulados en tiempo real en la consola. En una prueba de carga a gran escala se evitarían los &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; porque escribir en la consola consume recursos (I/O) y puede afectar ligeramente los resultados de rendimiento. Locust ya proporciona estadísticas detalladas en su interfaz web, que es la forma profesional de analizar los resultados, como vas a ver a continuación.&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
# Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura el número de usuarios -por ejemplo, 10-, la tasa de generación (cada cuánto tiempo se lanza un nuevo usuario)-por ejemplo, 1- y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;). Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
# En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {&amp;quot;WebsiteTestUser&amp;quot;: 10} (10 total users)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar una primera prueba para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía, comprobando el flujo completo desde el navegador del usuario. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1). Estas clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. Es probable que hayas visto pruebas unitarias en las que se utilizan mocks (a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;) para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones reales, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10122</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10122"/>
				<updated>2025-10-07T08:34:26Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Dependencias */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de pruebas software en una aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
python3.12 -m venv .venv&lt;br /&gt;
source .venv/bin/activate&lt;br /&gt;
pip3.12 install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/routes.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas. En una aplicación real esta lista global sería reemplazada por una base de datos (como ocurre en UVLHUB). Usar una variable global hace que los datos se pierdan cada vez que se reinicia el servidor y pueda no funcionar correctamente si múltiples usuarios acceden a la vez. Sin embargo, para aprender a escribir pruebas, es una simplificación muy útil&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella creando y visualizando las tareas usando primero el formulario web y luego también mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real. &lt;br /&gt;
&lt;br /&gt;
Por tanto, este archivo permite que las pruebas sean repetibles y aisladas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que se ha usado &amp;lt;code&amp;gt;sys.path.append&amp;lt;/code&amp;gt; por simplicidad, pero se considera una práctica un poco anticuada y frágil. Una alternativa más moderna y robusta, como has visto con Rosemary en UVLHUB, es convertir el proyecto en un paquete instalable. Para ello, se crearía un archivo &amp;lt;code&amp;gt;setup.py&amp;lt;/code&amp;gt; en la raíz del proyecto y luego se instalaría en modo editable con el comando &amp;lt;code&amp;gt;pip install -e .&amp;lt;/code&amp;gt;. De esta forma, pytest encontraría el paquete app automáticamente.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
Name              Stmts   Miss  Cover&lt;br /&gt;
----------------  ------  ----  -----&lt;br /&gt;
app/__init__.py        1      0   100%&lt;br /&gt;
app/app.py             6      0   100%&lt;br /&gt;
app/models.py          9      0   100%&lt;br /&gt;
app/routes.py         26      2    92%&lt;br /&gt;
TOTAL                 42      2    95%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
En relación al código utilizado, ten en cuenta que se ha usado &amp;lt;code&amp;gt;time.sleep(1)&amp;lt;/code&amp;gt; por simplicidad, pero es una práctica poco fiable. Una prueba puede fallar porque la red o el equipo van lentos y la página tarda más de 1 segundo en cargar. Si subes el tiempo (ej. &amp;lt;code&amp;gt;time.sleep(10)&amp;lt;/code&amp;gt;), haces las pruebas innecesariamente lentas. La solución es utilizar esperas explícitas (explicit waits), tal como se verás que se hace en UVLHUB, con las que se indica a Selenium que espere hasta que una condición se cumpla (por ejemplo, hasta que un elemento sea visible), con un tiempo máximo de espera.&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
    &lt;br /&gt;
Ejecutar el test exportado:&lt;br /&gt;
&lt;br /&gt;
Y ya puedes ejecutar el test exportado utilizando pytest:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest tests/test_selenium_ide.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto ejecutará el test generado por &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en tu navegador usando &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que en esta demostración usamos &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; para poder ver la actividad de los usuarios simulados en tiempo real en la consola. En una prueba de carga a gran escala se evitarían los &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; porque escribir en la consola consume recursos (I/O) y puede afectar ligeramente los resultados de rendimiento. Locust ya proporciona estadísticas detalladas en su interfaz web, que es la forma profesional de analizar los resultados, como vas a ver a continuación.&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
# Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura el número de usuarios -por ejemplo, 10-, la tasa de generación (cada cuánto tiempo se lanza un nuevo usuario)-por ejemplo, 1- y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;). Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
# En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {&amp;quot;WebsiteTestUser&amp;quot;: 10} (10 total users)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar una primera prueba para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía, comprobando el flujo completo desde el navegador del usuario. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1). Estas clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. Es probable que hayas visto pruebas unitarias en las que se utilizan mocks (a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;) para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones reales, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10120</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10120"/>
				<updated>2025-10-07T08:30:45Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Dependencias */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de pruebas software en una aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
python -m venv .venv&lt;br /&gt;
source .venv/bin/activate&lt;br /&gt;
pip install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/routes.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas. En una aplicación real esta lista global sería reemplazada por una base de datos (como ocurre en UVLHUB). Usar una variable global hace que los datos se pierdan cada vez que se reinicia el servidor y pueda no funcionar correctamente si múltiples usuarios acceden a la vez. Sin embargo, para aprender a escribir pruebas, es una simplificación muy útil&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella creando y visualizando las tareas usando primero el formulario web y luego también mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real. &lt;br /&gt;
&lt;br /&gt;
Por tanto, este archivo permite que las pruebas sean repetibles y aisladas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que se ha usado &amp;lt;code&amp;gt;sys.path.append&amp;lt;/code&amp;gt; por simplicidad, pero se considera una práctica un poco anticuada y frágil. Una alternativa más moderna y robusta, como has visto con Rosemary en UVLHUB, es convertir el proyecto en un paquete instalable. Para ello, se crearía un archivo &amp;lt;code&amp;gt;setup.py&amp;lt;/code&amp;gt; en la raíz del proyecto y luego se instalaría en modo editable con el comando &amp;lt;code&amp;gt;pip install -e .&amp;lt;/code&amp;gt;. De esta forma, pytest encontraría el paquete app automáticamente.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
Name              Stmts   Miss  Cover&lt;br /&gt;
----------------  ------  ----  -----&lt;br /&gt;
app/__init__.py        1      0   100%&lt;br /&gt;
app/app.py             6      0   100%&lt;br /&gt;
app/models.py          9      0   100%&lt;br /&gt;
app/routes.py         26      2    92%&lt;br /&gt;
TOTAL                 42      2    95%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
En relación al código utilizado, ten en cuenta que se ha usado &amp;lt;code&amp;gt;time.sleep(1)&amp;lt;/code&amp;gt; por simplicidad, pero es una práctica poco fiable. Una prueba puede fallar porque la red o el equipo van lentos y la página tarda más de 1 segundo en cargar. Si subes el tiempo (ej. &amp;lt;code&amp;gt;time.sleep(10)&amp;lt;/code&amp;gt;), haces las pruebas innecesariamente lentas. La solución es utilizar esperas explícitas (explicit waits), tal como se verás que se hace en UVLHUB, con las que se indica a Selenium que espere hasta que una condición se cumpla (por ejemplo, hasta que un elemento sea visible), con un tiempo máximo de espera.&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
    &lt;br /&gt;
Ejecutar el test exportado:&lt;br /&gt;
&lt;br /&gt;
Y ya puedes ejecutar el test exportado utilizando pytest:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest tests/test_selenium_ide.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto ejecutará el test generado por &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en tu navegador usando &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que en esta demostración usamos &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; para poder ver la actividad de los usuarios simulados en tiempo real en la consola. En una prueba de carga a gran escala se evitarían los &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; porque escribir en la consola consume recursos (I/O) y puede afectar ligeramente los resultados de rendimiento. Locust ya proporciona estadísticas detalladas en su interfaz web, que es la forma profesional de analizar los resultados, como vas a ver a continuación.&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
# Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura el número de usuarios -por ejemplo, 10-, la tasa de generación (cada cuánto tiempo se lanza un nuevo usuario)-por ejemplo, 1- y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;). Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
# En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {&amp;quot;WebsiteTestUser&amp;quot;: 10} (10 total users)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar una primera prueba para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía, comprobando el flujo completo desde el navegador del usuario. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1). Estas clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. Es probable que hayas visto pruebas unitarias en las que se utilizan mocks (a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;) para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones reales, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10119</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10119"/>
				<updated>2025-10-07T07:47:12Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Algunas cuestiones que puedes investigar */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de pruebas software en una aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pip install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/routes.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas. En una aplicación real esta lista global sería reemplazada por una base de datos (como ocurre en UVLHUB). Usar una variable global hace que los datos se pierdan cada vez que se reinicia el servidor y pueda no funcionar correctamente si múltiples usuarios acceden a la vez. Sin embargo, para aprender a escribir pruebas, es una simplificación muy útil&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella creando y visualizando las tareas usando primero el formulario web y luego también mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real. &lt;br /&gt;
&lt;br /&gt;
Por tanto, este archivo permite que las pruebas sean repetibles y aisladas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que se ha usado &amp;lt;code&amp;gt;sys.path.append&amp;lt;/code&amp;gt; por simplicidad, pero se considera una práctica un poco anticuada y frágil. Una alternativa más moderna y robusta, como has visto con Rosemary en UVLHUB, es convertir el proyecto en un paquete instalable. Para ello, se crearía un archivo &amp;lt;code&amp;gt;setup.py&amp;lt;/code&amp;gt; en la raíz del proyecto y luego se instalaría en modo editable con el comando &amp;lt;code&amp;gt;pip install -e .&amp;lt;/code&amp;gt;. De esta forma, pytest encontraría el paquete app automáticamente.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
Name              Stmts   Miss  Cover&lt;br /&gt;
----------------  ------  ----  -----&lt;br /&gt;
app/__init__.py        1      0   100%&lt;br /&gt;
app/app.py             6      0   100%&lt;br /&gt;
app/models.py          9      0   100%&lt;br /&gt;
app/routes.py         26      2    92%&lt;br /&gt;
TOTAL                 42      2    95%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
En relación al código utilizado, ten en cuenta que se ha usado &amp;lt;code&amp;gt;time.sleep(1)&amp;lt;/code&amp;gt; por simplicidad, pero es una práctica poco fiable. Una prueba puede fallar porque la red o el equipo van lentos y la página tarda más de 1 segundo en cargar. Si subes el tiempo (ej. &amp;lt;code&amp;gt;time.sleep(10)&amp;lt;/code&amp;gt;), haces las pruebas innecesariamente lentas. La solución es utilizar esperas explícitas (explicit waits), tal como se verás que se hace en UVLHUB, con las que se indica a Selenium que espere hasta que una condición se cumpla (por ejemplo, hasta que un elemento sea visible), con un tiempo máximo de espera.&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
    &lt;br /&gt;
Ejecutar el test exportado:&lt;br /&gt;
&lt;br /&gt;
Y ya puedes ejecutar el test exportado utilizando pytest:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest tests/test_selenium_ide.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto ejecutará el test generado por &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en tu navegador usando &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que en esta demostración usamos &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; para poder ver la actividad de los usuarios simulados en tiempo real en la consola. En una prueba de carga a gran escala se evitarían los &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; porque escribir en la consola consume recursos (I/O) y puede afectar ligeramente los resultados de rendimiento. Locust ya proporciona estadísticas detalladas en su interfaz web, que es la forma profesional de analizar los resultados, como vas a ver a continuación.&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
# Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura el número de usuarios -por ejemplo, 10-, la tasa de generación (cada cuánto tiempo se lanza un nuevo usuario)-por ejemplo, 1- y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;). Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
# En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {&amp;quot;WebsiteTestUser&amp;quot;: 10} (10 total users)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar una primera prueba para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía, comprobando el flujo completo desde el navegador del usuario. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1). Estas clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. Es probable que hayas visto pruebas unitarias en las que se utilizan mocks (a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;) para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones reales, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10118</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10118"/>
				<updated>2025-10-07T07:46:42Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Algunas cuestiones que puedes investigar */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de pruebas software en una aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pip install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/routes.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas. En una aplicación real esta lista global sería reemplazada por una base de datos (como ocurre en UVLHUB). Usar una variable global hace que los datos se pierdan cada vez que se reinicia el servidor y pueda no funcionar correctamente si múltiples usuarios acceden a la vez. Sin embargo, para aprender a escribir pruebas, es una simplificación muy útil&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella creando y visualizando las tareas usando primero el formulario web y luego también mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real. &lt;br /&gt;
&lt;br /&gt;
Por tanto, este archivo permite que las pruebas sean repetibles y aisladas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que se ha usado &amp;lt;code&amp;gt;sys.path.append&amp;lt;/code&amp;gt; por simplicidad, pero se considera una práctica un poco anticuada y frágil. Una alternativa más moderna y robusta, como has visto con Rosemary en UVLHUB, es convertir el proyecto en un paquete instalable. Para ello, se crearía un archivo &amp;lt;code&amp;gt;setup.py&amp;lt;/code&amp;gt; en la raíz del proyecto y luego se instalaría en modo editable con el comando &amp;lt;code&amp;gt;pip install -e .&amp;lt;/code&amp;gt;. De esta forma, pytest encontraría el paquete app automáticamente.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
Name              Stmts   Miss  Cover&lt;br /&gt;
----------------  ------  ----  -----&lt;br /&gt;
app/__init__.py        1      0   100%&lt;br /&gt;
app/app.py             6      0   100%&lt;br /&gt;
app/models.py          9      0   100%&lt;br /&gt;
app/routes.py         26      2    92%&lt;br /&gt;
TOTAL                 42      2    95%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
En relación al código utilizado, ten en cuenta que se ha usado &amp;lt;code&amp;gt;time.sleep(1)&amp;lt;/code&amp;gt; por simplicidad, pero es una práctica poco fiable. Una prueba puede fallar porque la red o el equipo van lentos y la página tarda más de 1 segundo en cargar. Si subes el tiempo (ej. &amp;lt;code&amp;gt;time.sleep(10)&amp;lt;/code&amp;gt;), haces las pruebas innecesariamente lentas. La solución es utilizar esperas explícitas (explicit waits), tal como se verás que se hace en UVLHUB, con las que se indica a Selenium que espere hasta que una condición se cumpla (por ejemplo, hasta que un elemento sea visible), con un tiempo máximo de espera.&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
    &lt;br /&gt;
Ejecutar el test exportado:&lt;br /&gt;
&lt;br /&gt;
Y ya puedes ejecutar el test exportado utilizando pytest:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest tests/test_selenium_ide.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto ejecutará el test generado por &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en tu navegador usando &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que en esta demostración usamos &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; para poder ver la actividad de los usuarios simulados en tiempo real en la consola. En una prueba de carga a gran escala se evitarían los &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; porque escribir en la consola consume recursos (I/O) y puede afectar ligeramente los resultados de rendimiento. Locust ya proporciona estadísticas detalladas en su interfaz web, que es la forma profesional de analizar los resultados, como vas a ver a continuación.&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
# Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura el número de usuarios -por ejemplo, 10-, la tasa de generación (cada cuánto tiempo se lanza un nuevo usuario)-por ejemplo, 1- y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;). Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
# En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {&amp;quot;WebsiteTestUser&amp;quot;: 10} (10 total users)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar una primera prueba para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía, comprobando el flujo completo desde el navegador del usuario. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1). Estas clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. Es probable que hayas visto pruebas unitarias en las que se utilizan mocks —a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;— para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones reales, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10117</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10117"/>
				<updated>2025-10-07T07:46:14Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Un ejemplo sencillo para ayudarte a arrancar */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de pruebas software en una aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pip install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/routes.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas. En una aplicación real esta lista global sería reemplazada por una base de datos (como ocurre en UVLHUB). Usar una variable global hace que los datos se pierdan cada vez que se reinicia el servidor y pueda no funcionar correctamente si múltiples usuarios acceden a la vez. Sin embargo, para aprender a escribir pruebas, es una simplificación muy útil&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella creando y visualizando las tareas usando primero el formulario web y luego también mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real. &lt;br /&gt;
&lt;br /&gt;
Por tanto, este archivo permite que las pruebas sean repetibles y aisladas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que se ha usado &amp;lt;code&amp;gt;sys.path.append&amp;lt;/code&amp;gt; por simplicidad, pero se considera una práctica un poco anticuada y frágil. Una alternativa más moderna y robusta, como has visto con Rosemary en UVLHUB, es convertir el proyecto en un paquete instalable. Para ello, se crearía un archivo &amp;lt;code&amp;gt;setup.py&amp;lt;/code&amp;gt; en la raíz del proyecto y luego se instalaría en modo editable con el comando &amp;lt;code&amp;gt;pip install -e .&amp;lt;/code&amp;gt;. De esta forma, pytest encontraría el paquete app automáticamente.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
Name              Stmts   Miss  Cover&lt;br /&gt;
----------------  ------  ----  -----&lt;br /&gt;
app/__init__.py        1      0   100%&lt;br /&gt;
app/app.py             6      0   100%&lt;br /&gt;
app/models.py          9      0   100%&lt;br /&gt;
app/routes.py         26      2    92%&lt;br /&gt;
TOTAL                 42      2    95%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
En relación al código utilizado, ten en cuenta que se ha usado &amp;lt;code&amp;gt;time.sleep(1)&amp;lt;/code&amp;gt; por simplicidad, pero es una práctica poco fiable. Una prueba puede fallar porque la red o el equipo van lentos y la página tarda más de 1 segundo en cargar. Si subes el tiempo (ej. &amp;lt;code&amp;gt;time.sleep(10)&amp;lt;/code&amp;gt;), haces las pruebas innecesariamente lentas. La solución es utilizar esperas explícitas (explicit waits), tal como se verás que se hace en UVLHUB, con las que se indica a Selenium que espere hasta que una condición se cumpla (por ejemplo, hasta que un elemento sea visible), con un tiempo máximo de espera.&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
    &lt;br /&gt;
Ejecutar el test exportado:&lt;br /&gt;
&lt;br /&gt;
Y ya puedes ejecutar el test exportado utilizando pytest:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest tests/test_selenium_ide.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto ejecutará el test generado por &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en tu navegador usando &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que en esta demostración usamos &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; para poder ver la actividad de los usuarios simulados en tiempo real en la consola. En una prueba de carga a gran escala se evitarían los &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; porque escribir en la consola consume recursos (I/O) y puede afectar ligeramente los resultados de rendimiento. Locust ya proporciona estadísticas detalladas en su interfaz web, que es la forma profesional de analizar los resultados, como vas a ver a continuación.&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
# Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura el número de usuarios -por ejemplo, 10-, la tasa de generación (cada cuánto tiempo se lanza un nuevo usuario)-por ejemplo, 1- y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;). Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
# En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {&amp;quot;WebsiteTestUser&amp;quot;: 10} (10 total users)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar una primera prueba para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía, comprobando el flujo completo desde el navegador del usuario. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1. Estas clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. Es probable que hayas visto pruebas unitarias en las que se utilizan mocks —a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;— para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones reales, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10116</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10116"/>
				<updated>2025-10-07T07:45:04Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Un ejemplo sencillo para ayudarte a arrancar */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de pruebas software en una aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pip install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/routes.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas. En una aplicación real esta lista global sería reemplazada por una base de datos (como ocurre en UVLHUB). Usar una variable global hace que los datos se pierdan cada vez que se reinicia el servidor y pueda no funcionar correctamente si múltiples usuarios acceden a la vez. Sin embargo, para aprender a escribir pruebas, es una simplificación muy útil&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella creando y visualizando las tareas usando primero el formulario web y luego también mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real. &lt;br /&gt;
&lt;br /&gt;
Por tanto, este archivo permite que las pruebas sean repetibles y aisladas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que se ha usado &amp;lt;code&amp;gt;sys.path.append&amp;lt;/code&amp;gt; por simplicidad, pero se considera una práctica un poco anticuada y frágil. Una alternativa más moderna y robusta, como has visto con Rosemary en UVLHUB, es convertir el proyecto en un paquete instalable. Para ello, se crearía un archivo &amp;lt;code&amp;gt;setup.py&amp;lt;/code&amp;gt; en la raíz del proyecto y luego se instalaría en modo editable con el comando &amp;lt;code&amp;gt;pip install -e .&amp;lt;/code&amp;gt;. De esta forma, pytest encontraría el paquete app automáticamente.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
Name              Stmts   Miss  Cover&lt;br /&gt;
----------------  ------  ----  -----&lt;br /&gt;
app/__init__.py        1      0   100%&lt;br /&gt;
app/app.py             6      0   100%&lt;br /&gt;
app/models.py          9      0   100%&lt;br /&gt;
app/routes.py         26      2    92%&lt;br /&gt;
TOTAL                 42      2    95%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
En relación al código utilizado, ten en cuenta que se ha usado &amp;lt;code&amp;gt;time.sleep(1)&amp;lt;/code&amp;gt; por simplicidad, pero es una práctica poco fiable. Una prueba puede fallar porque la red o el equipo van lentos y la página tarda más de 1 segundo en cargar. Si subes el tiempo (ej. &amp;lt;code&amp;gt;time.sleep(10)&amp;lt;/code&amp;gt;), haces las pruebas innecesariamente lentas. La solución es utilizar esperas explícitas (explicit waits), tal como se verás que se hace en UVLHUB, con las que se indica a Selenium que espere hasta que una condición se cumpla (por ejemplo, hasta que un elemento sea visible), con un tiempo máximo de espera.&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
    &lt;br /&gt;
Ejecutar el test exportado:&lt;br /&gt;
&lt;br /&gt;
Y ya puedes ejecutar el test exportado utilizando pytest:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest tests/test_selenium_ide.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto ejecutará el test generado por &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en tu navegador usando &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que en esta demostración usamos &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; para poder ver la actividad de los usuarios simulados en tiempo real en la consola. En una prueba de carga a gran escala se evitarían los &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; porque escribir en la consola consume recursos (I/O) y puede afectar ligeramente los resultados de rendimiento. Locust ya proporciona estadísticas detalladas en su interfaz web, que es la forma profesional de analizar los resultados, como vas a ver a continuación.&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
# Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura el número de usuarios -por ejemplo, 10-, la tasa de generación (cada cuánto tiempo se lanza un nuevo usuario)-por ejemplo, 1- y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;). Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
# En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {&amp;quot;WebsiteTestUser&amp;quot;: 10} (10 total users)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar pruebas para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fíjate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía, comprobando el flujo completo desde el navegador del usuario. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1. Estas clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. Es probable que hayas visto pruebas unitarias en las que se utilizan mocks —a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;— para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones reales, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10115</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10115"/>
				<updated>2025-10-07T07:42:47Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Algunas cuestiones que puedes investigar */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de pruebas software en una aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pip install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/routes.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas. En una aplicación real esta lista global sería reemplazada por una base de datos (como ocurre en UVLHUB). Usar una variable global hace que los datos se pierdan cada vez que se reinicia el servidor y pueda no funcionar correctamente si múltiples usuarios acceden a la vez. Sin embargo, para aprender a escribir pruebas, es una simplificación muy útil&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella creando y visualizando las tareas usando primero el formulario web y luego también mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real. &lt;br /&gt;
&lt;br /&gt;
Por tanto, este archivo permite que las pruebas sean repetibles y aisladas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que se ha usado &amp;lt;code&amp;gt;sys.path.append&amp;lt;/code&amp;gt; por simplicidad, pero se considera una práctica un poco anticuada y frágil. Una alternativa más moderna y robusta, como has visto con Rosemary en UVLHUB, es convertir el proyecto en un paquete instalable. Para ello, se crearía un archivo &amp;lt;code&amp;gt;setup.py&amp;lt;/code&amp;gt; en la raíz del proyecto y luego se instalaría en modo editable con el comando &amp;lt;code&amp;gt;pip install -e .&amp;lt;/code&amp;gt;. De esta forma, pytest encontraría el paquete app automáticamente.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
Name              Stmts   Miss  Cover&lt;br /&gt;
----------------  ------  ----  -----&lt;br /&gt;
app/__init__.py        1      0   100%&lt;br /&gt;
app/app.py             6      0   100%&lt;br /&gt;
app/models.py          9      0   100%&lt;br /&gt;
app/routes.py         26      2    92%&lt;br /&gt;
TOTAL                 42      2    95%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
En relación al código utilizado, ten en cuenta que se ha usado &amp;lt;code&amp;gt;time.sleep(1)&amp;lt;/code&amp;gt; por simplicidad, pero es una práctica poco fiable. Una prueba puede fallar porque la red o el equipo van lentos y la página tarda más de 1 segundo en cargar. Si subes el tiempo (ej. &amp;lt;code&amp;gt;time.sleep(10)&amp;lt;/code&amp;gt;), haces las pruebas innecesariamente lentas. La solución es utilizar esperas explícitas (explicit waits), tal como se verás que se hace en UVLHUB, con las que se indica a Selenium que espere hasta que una condición se cumpla (por ejemplo, hasta que un elemento sea visible), con un tiempo máximo de espera.&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
    &lt;br /&gt;
Ejecutar el test exportado:&lt;br /&gt;
&lt;br /&gt;
Y ya puedes ejecutar el test exportado utilizando pytest:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest tests/test_selenium_ide.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto ejecutará el test generado por &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en tu navegador usando &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que en esta demostración usamos &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; para poder ver la actividad de los usuarios simulados en tiempo real en la consola. En una prueba de carga a gran escala se evitarían los &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; porque escribir en la consola consume recursos (I/O) y puede afectar ligeramente los resultados de rendimiento. Locust ya proporciona estadísticas detalladas en su interfaz web, que es la forma profesional de analizar los resultados, como vas a ver a continuación.&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
# Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura el número de usuarios -por ejemplo, 10-, la tasa de generación (cada cuánto tiempo se lanza un nuevo usuario)-por ejemplo, 1- y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;). Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
# En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {&amp;quot;WebsiteTestUser&amp;quot;: 10} (10 total users)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar pruebas unitarias para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fijate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1. Estas clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. Es probable que hayas visto pruebas unitarias en las que se utilizan mocks —a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;— para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones reales, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10114</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10114"/>
				<updated>2025-10-07T07:35:45Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Configuración del entorno de pruebas con conftest.py */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de pruebas software en una aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pip install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/routes.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas. En una aplicación real esta lista global sería reemplazada por una base de datos (como ocurre en UVLHUB). Usar una variable global hace que los datos se pierdan cada vez que se reinicia el servidor y pueda no funcionar correctamente si múltiples usuarios acceden a la vez. Sin embargo, para aprender a escribir pruebas, es una simplificación muy útil&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella creando y visualizando las tareas usando primero el formulario web y luego también mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real. &lt;br /&gt;
&lt;br /&gt;
Por tanto, este archivo permite que las pruebas sean repetibles y aisladas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que se ha usado &amp;lt;code&amp;gt;sys.path.append&amp;lt;/code&amp;gt; por simplicidad, pero se considera una práctica un poco anticuada y frágil. Una alternativa más moderna y robusta, como has visto con Rosemary en UVLHUB, es convertir el proyecto en un paquete instalable. Para ello, se crearía un archivo &amp;lt;code&amp;gt;setup.py&amp;lt;/code&amp;gt; en la raíz del proyecto y luego se instalaría en modo editable con el comando &amp;lt;code&amp;gt;pip install -e .&amp;lt;/code&amp;gt;. De esta forma, pytest encontraría el paquete app automáticamente.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
Name              Stmts   Miss  Cover&lt;br /&gt;
----------------  ------  ----  -----&lt;br /&gt;
app/__init__.py        1      0   100%&lt;br /&gt;
app/app.py             6      0   100%&lt;br /&gt;
app/models.py          9      0   100%&lt;br /&gt;
app/routes.py         26      2    92%&lt;br /&gt;
TOTAL                 42      2    95%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
En relación al código utilizado, ten en cuenta que se ha usado &amp;lt;code&amp;gt;time.sleep(1)&amp;lt;/code&amp;gt; por simplicidad, pero es una práctica poco fiable. Una prueba puede fallar porque la red o el equipo van lentos y la página tarda más de 1 segundo en cargar. Si subes el tiempo (ej. &amp;lt;code&amp;gt;time.sleep(10)&amp;lt;/code&amp;gt;), haces las pruebas innecesariamente lentas. La solución es utilizar esperas explícitas (explicit waits), tal como se verás que se hace en UVLHUB, con las que se indica a Selenium que espere hasta que una condición se cumpla (por ejemplo, hasta que un elemento sea visible), con un tiempo máximo de espera.&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
    &lt;br /&gt;
Ejecutar el test exportado:&lt;br /&gt;
&lt;br /&gt;
Y ya puedes ejecutar el test exportado utilizando pytest:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest tests/test_selenium_ide.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto ejecutará el test generado por &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en tu navegador usando &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que en esta demostración usamos &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; para poder ver la actividad de los usuarios simulados en tiempo real en la consola. En una prueba de carga a gran escala se evitarían los &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; porque escribir en la consola consume recursos (I/O) y puede afectar ligeramente los resultados de rendimiento. Locust ya proporciona estadísticas detalladas en su interfaz web, que es la forma profesional de analizar los resultados, como vas a ver a continuación.&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
# Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura el número de usuarios -por ejemplo, 10-, la tasa de generación (cada cuánto tiempo se lanza un nuevo usuario)-por ejemplo, 1- y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;). Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
# En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {&amp;quot;WebsiteTestUser&amp;quot;: 10} (10 total users)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar pruebas unitarias para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fijate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas unitarias necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1). Estas clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. En las pruebas unitarias se utilizan mocks —a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;— para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones grandes, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10113</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10113"/>
				<updated>2025-10-07T07:28:55Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Pruebas de interfaz con Selenium */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de pruebas software en una aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pip install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/routes.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas. En una aplicación real esta lista global sería reemplazada por una base de datos (como ocurre en UVLHUB). Usar una variable global hace que los datos se pierdan cada vez que se reinicia el servidor y pueda no funcionar correctamente si múltiples usuarios acceden a la vez. Sin embargo, para aprender a escribir pruebas, es una simplificación muy útil&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella creando y visualizando las tareas usando primero el formulario web y luego también mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real. &lt;br /&gt;
&lt;br /&gt;
Por tanto, este archivo permite que las pruebas sean repetibles y aisladas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
Name              Stmts   Miss  Cover&lt;br /&gt;
----------------  ------  ----  -----&lt;br /&gt;
app/__init__.py        1      0   100%&lt;br /&gt;
app/app.py             6      0   100%&lt;br /&gt;
app/models.py          9      0   100%&lt;br /&gt;
app/routes.py         26      2    92%&lt;br /&gt;
TOTAL                 42      2    95%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
En relación al código utilizado, ten en cuenta que se ha usado &amp;lt;code&amp;gt;time.sleep(1)&amp;lt;/code&amp;gt; por simplicidad, pero es una práctica poco fiable. Una prueba puede fallar porque la red o el equipo van lentos y la página tarda más de 1 segundo en cargar. Si subes el tiempo (ej. &amp;lt;code&amp;gt;time.sleep(10)&amp;lt;/code&amp;gt;), haces las pruebas innecesariamente lentas. La solución es utilizar esperas explícitas (explicit waits), tal como se verás que se hace en UVLHUB, con las que se indica a Selenium que espere hasta que una condición se cumpla (por ejemplo, hasta que un elemento sea visible), con un tiempo máximo de espera.&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
    &lt;br /&gt;
Ejecutar el test exportado:&lt;br /&gt;
&lt;br /&gt;
Y ya puedes ejecutar el test exportado utilizando pytest:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest tests/test_selenium_ide.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto ejecutará el test generado por &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en tu navegador usando &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que en esta demostración usamos &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; para poder ver la actividad de los usuarios simulados en tiempo real en la consola. En una prueba de carga a gran escala se evitarían los &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; porque escribir en la consola consume recursos (I/O) y puede afectar ligeramente los resultados de rendimiento. Locust ya proporciona estadísticas detalladas en su interfaz web, que es la forma profesional de analizar los resultados, como vas a ver a continuación.&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
# Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura el número de usuarios -por ejemplo, 10-, la tasa de generación (cada cuánto tiempo se lanza un nuevo usuario)-por ejemplo, 1- y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;). Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
# En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {&amp;quot;WebsiteTestUser&amp;quot;: 10} (10 total users)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar pruebas unitarias para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fijate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas unitarias necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1). Estas clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. En las pruebas unitarias se utilizan mocks —a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;— para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones grandes, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10112</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10112"/>
				<updated>2025-10-07T07:25:45Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Pruebas de carga con Locust */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de pruebas software en una aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pip install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/routes.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas. En una aplicación real esta lista global sería reemplazada por una base de datos (como ocurre en UVLHUB). Usar una variable global hace que los datos se pierdan cada vez que se reinicia el servidor y pueda no funcionar correctamente si múltiples usuarios acceden a la vez. Sin embargo, para aprender a escribir pruebas, es una simplificación muy útil&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella creando y visualizando las tareas usando primero el formulario web y luego también mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real. &lt;br /&gt;
&lt;br /&gt;
Por tanto, este archivo permite que las pruebas sean repetibles y aisladas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
Name              Stmts   Miss  Cover&lt;br /&gt;
----------------  ------  ----  -----&lt;br /&gt;
app/__init__.py        1      0   100%&lt;br /&gt;
app/app.py             6      0   100%&lt;br /&gt;
app/models.py          9      0   100%&lt;br /&gt;
app/routes.py         26      2    92%&lt;br /&gt;
TOTAL                 42      2    95%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
    &lt;br /&gt;
Ejecutar el test exportado:&lt;br /&gt;
&lt;br /&gt;
Y ya puedes ejecutar el test exportado utilizando pytest:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest tests/test_selenium_ide.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto ejecutará el test generado por &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en tu navegador usando &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ten en cuenta que en esta demostración usamos &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; para poder ver la actividad de los usuarios simulados en tiempo real en la consola. En una prueba de carga a gran escala se evitarían los &amp;lt;code&amp;gt;print()&amp;lt;/code&amp;gt; porque escribir en la consola consume recursos (I/O) y puede afectar ligeramente los resultados de rendimiento. Locust ya proporciona estadísticas detalladas en su interfaz web, que es la forma profesional de analizar los resultados, como vas a ver a continuación.&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
# Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura el número de usuarios -por ejemplo, 10-, la tasa de generación (cada cuánto tiempo se lanza un nuevo usuario)-por ejemplo, 1- y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;). Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
# En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {&amp;quot;WebsiteTestUser&amp;quot;: 10} (10 total users)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar pruebas unitarias para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fijate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas unitarias necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1). Estas clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. En las pruebas unitarias se utilizan mocks —a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;— para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones grandes, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10111</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10111"/>
				<updated>2025-10-07T07:21:39Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Configuración del entorno de pruebas con conftest.py */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de pruebas software en una aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pip install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/routes.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas. En una aplicación real esta lista global sería reemplazada por una base de datos (como ocurre en UVLHUB). Usar una variable global hace que los datos se pierdan cada vez que se reinicia el servidor y pueda no funcionar correctamente si múltiples usuarios acceden a la vez. Sin embargo, para aprender a escribir pruebas, es una simplificación muy útil&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella creando y visualizando las tareas usando primero el formulario web y luego también mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real. &lt;br /&gt;
&lt;br /&gt;
Por tanto, este archivo permite que las pruebas sean repetibles y aisladas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
Name              Stmts   Miss  Cover&lt;br /&gt;
----------------  ------  ----  -----&lt;br /&gt;
app/__init__.py        1      0   100%&lt;br /&gt;
app/app.py             6      0   100%&lt;br /&gt;
app/models.py          9      0   100%&lt;br /&gt;
app/routes.py         26      2    92%&lt;br /&gt;
TOTAL                 42      2    95%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
    &lt;br /&gt;
Ejecutar el test exportado:&lt;br /&gt;
&lt;br /&gt;
Y ya puedes ejecutar el test exportado utilizando pytest:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest tests/test_selenium_ide.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto ejecutará el test generado por &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en tu navegador usando &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
# Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura el número de usuarios -por ejemplo, 10-, la tasa de generación (cada cuánto tiempo se lanza un nuevo usuario)-por ejemplo, 1- y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;). Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
# En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {&amp;quot;WebsiteTestUser&amp;quot;: 10} (10 total users)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar pruebas unitarias para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fijate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas unitarias necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1). Estas clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. En las pruebas unitarias se utilizan mocks —a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;— para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones grandes, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10110</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10110"/>
				<updated>2025-10-07T07:20:27Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Código app/__init__.py: */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de pruebas software en una aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pip install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/routes.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas. En una aplicación real esta lista global sería reemplazada por una base de datos (como ocurre en UVLHUB). Usar una variable global hace que los datos se pierdan cada vez que se reinicia el servidor y pueda no funcionar correctamente si múltiples usuarios acceden a la vez. Sin embargo, para aprender a escribir pruebas, es una simplificación muy útil&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella creando y visualizando las tareas usando primero el formulario web y luego también mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real; así podemos probar las rutas de la aplicación de forma rápida y controlada. &lt;br /&gt;
&lt;br /&gt;
En conjunto, este archivo hace que las pruebas sean repetibles, aisladas y automáticas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
Name              Stmts   Miss  Cover&lt;br /&gt;
----------------  ------  ----  -----&lt;br /&gt;
app/__init__.py        1      0   100%&lt;br /&gt;
app/app.py             6      0   100%&lt;br /&gt;
app/models.py          9      0   100%&lt;br /&gt;
app/routes.py         26      2    92%&lt;br /&gt;
TOTAL                 42      2    95%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
    &lt;br /&gt;
Ejecutar el test exportado:&lt;br /&gt;
&lt;br /&gt;
Y ya puedes ejecutar el test exportado utilizando pytest:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest tests/test_selenium_ide.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto ejecutará el test generado por &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en tu navegador usando &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
# Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura el número de usuarios -por ejemplo, 10-, la tasa de generación (cada cuánto tiempo se lanza un nuevo usuario)-por ejemplo, 1- y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;). Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
# En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {&amp;quot;WebsiteTestUser&amp;quot;: 10} (10 total users)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar pruebas unitarias para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fijate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas unitarias necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1). Estas clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. En las pruebas unitarias se utilizan mocks —a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;— para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones grandes, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10109</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10109"/>
				<updated>2025-10-07T07:18:47Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Código app/models.py: */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de pruebas software en una aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pip install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# app/__init__.py&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/routes.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas. En una aplicación real esta lista global sería reemplazada por una base de datos (como ocurre en UVLHUB). Usar una variable global hace que los datos se pierdan cada vez que se reinicia el servidor y pueda no funcionar correctamente si múltiples usuarios acceden a la vez. Sin embargo, para aprender a escribir pruebas, es una simplificación muy útil&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella creando y visualizando las tareas usando primero el formulario web y luego también mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real; así podemos probar las rutas de la aplicación de forma rápida y controlada. &lt;br /&gt;
&lt;br /&gt;
En conjunto, este archivo hace que las pruebas sean repetibles, aisladas y automáticas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
Name              Stmts   Miss  Cover&lt;br /&gt;
----------------  ------  ----  -----&lt;br /&gt;
app/__init__.py        1      0   100%&lt;br /&gt;
app/app.py             6      0   100%&lt;br /&gt;
app/models.py          9      0   100%&lt;br /&gt;
app/routes.py         26      2    92%&lt;br /&gt;
TOTAL                 42      2    95%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
    &lt;br /&gt;
Ejecutar el test exportado:&lt;br /&gt;
&lt;br /&gt;
Y ya puedes ejecutar el test exportado utilizando pytest:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest tests/test_selenium_ide.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto ejecutará el test generado por &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en tu navegador usando &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
# Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura el número de usuarios -por ejemplo, 10-, la tasa de generación (cada cuánto tiempo se lanza un nuevo usuario)-por ejemplo, 1- y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;). Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
# En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {&amp;quot;WebsiteTestUser&amp;quot;: 10} (10 total users)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar pruebas unitarias para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fijate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas unitarias necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1). Estas clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. En las pruebas unitarias se utilizan mocks —a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;— para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones grandes, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10108</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10108"/>
				<updated>2025-10-07T07:17:01Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Estructura del proyecto */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de pruebas software en una aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pip install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# app/__init__.py&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/routes.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella creando y visualizando las tareas usando primero el formulario web y luego también mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real; así podemos probar las rutas de la aplicación de forma rápida y controlada. &lt;br /&gt;
&lt;br /&gt;
En conjunto, este archivo hace que las pruebas sean repetibles, aisladas y automáticas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
Name              Stmts   Miss  Cover&lt;br /&gt;
----------------  ------  ----  -----&lt;br /&gt;
app/__init__.py        1      0   100%&lt;br /&gt;
app/app.py             6      0   100%&lt;br /&gt;
app/models.py          9      0   100%&lt;br /&gt;
app/routes.py         26      2    92%&lt;br /&gt;
TOTAL                 42      2    95%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
    &lt;br /&gt;
Ejecutar el test exportado:&lt;br /&gt;
&lt;br /&gt;
Y ya puedes ejecutar el test exportado utilizando pytest:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest tests/test_selenium_ide.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto ejecutará el test generado por &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en tu navegador usando &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
# Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura el número de usuarios -por ejemplo, 10-, la tasa de generación (cada cuánto tiempo se lanza un nuevo usuario)-por ejemplo, 1- y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;). Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
# En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {&amp;quot;WebsiteTestUser&amp;quot;: 10} (10 total users)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar pruebas unitarias para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fijate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas unitarias necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1). Estas clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. En las pruebas unitarias se utilizan mocks —a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;— para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones grandes, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10107</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10107"/>
				<updated>2025-10-07T07:16:33Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Código app/app.py: */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de pruebas software en una aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pip install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# app/__init__.py&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/routes.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella creando y visualizando las tareas usando primero el formulario web y luego también mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real; así podemos probar las rutas de la aplicación de forma rápida y controlada. &lt;br /&gt;
&lt;br /&gt;
En conjunto, este archivo hace que las pruebas sean repetibles, aisladas y automáticas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
Name              Stmts   Miss  Cover&lt;br /&gt;
----------------  ------  ----  -----&lt;br /&gt;
app/__init__.py        1      0   100%&lt;br /&gt;
app/app.py             6      0   100%&lt;br /&gt;
app/models.py          9      0   100%&lt;br /&gt;
app/routes.py         26      2    92%&lt;br /&gt;
TOTAL                 42      2    95%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
    &lt;br /&gt;
Ejecutar el test exportado:&lt;br /&gt;
&lt;br /&gt;
Y ya puedes ejecutar el test exportado utilizando pytest:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest tests/test_selenium_ide.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto ejecutará el test generado por &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en tu navegador usando &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
# Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura el número de usuarios -por ejemplo, 10-, la tasa de generación (cada cuánto tiempo se lanza un nuevo usuario)-por ejemplo, 1- y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;). Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
# En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {&amp;quot;WebsiteTestUser&amp;quot;: 10} (10 total users)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar pruebas unitarias para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fijate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas unitarias necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1). Estas clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. En las pruebas unitarias se utilizan mocks —a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;— para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones grandes, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10105</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10105"/>
				<updated>2025-10-06T20:08:03Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de pruebas software en una aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pip install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# app/__init__.py&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella creando y visualizando las tareas usando primero el formulario web y luego también mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real; así podemos probar las rutas de la aplicación de forma rápida y controlada. &lt;br /&gt;
&lt;br /&gt;
En conjunto, este archivo hace que las pruebas sean repetibles, aisladas y automáticas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
Name              Stmts   Miss  Cover&lt;br /&gt;
----------------  ------  ----  -----&lt;br /&gt;
app/__init__.py        1      0   100%&lt;br /&gt;
app/app.py             6      0   100%&lt;br /&gt;
app/models.py          9      0   100%&lt;br /&gt;
app/routes.py         26      2    92%&lt;br /&gt;
TOTAL                 42      2    95%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
    &lt;br /&gt;
Ejecutar el test exportado:&lt;br /&gt;
&lt;br /&gt;
Y ya puedes ejecutar el test exportado utilizando pytest:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest tests/test_selenium_ide.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto ejecutará el test generado por &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en tu navegador usando &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
# Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura el número de usuarios -por ejemplo, 10-, la tasa de generación (cada cuánto tiempo se lanza un nuevo usuario)-por ejemplo, 1- y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;). Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
# En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {&amp;quot;WebsiteTestUser&amp;quot;: 10} (10 total users)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar pruebas unitarias para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fijate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas unitarias necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1). Estas clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. En las pruebas unitarias se utilizan mocks —a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;— para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones grandes, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10104</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10104"/>
				<updated>2025-10-06T17:44:45Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: /* Pruebas de interfaz con Selenium */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de Pruebas de Software en una Aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pip install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la Aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# app/__init__.py&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella creando y visualizando las tareas usando primero el formulario web y luego también mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real; así podemos probar las rutas de la aplicación de forma rápida y controlada. &lt;br /&gt;
&lt;br /&gt;
En conjunto, este archivo hace que las pruebas sean repetibles, aisladas y automáticas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    Equivalente a preparar datos iniciales en la base de datos en UVLHUB.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
Name              Stmts   Miss  Cover&lt;br /&gt;
----------------  ------  ----  -----&lt;br /&gt;
app/__init__.py        1      0   100%&lt;br /&gt;
app/app.py             6      0   100%&lt;br /&gt;
app/models.py          9      0   100%&lt;br /&gt;
app/routes.py         26      2    92%&lt;br /&gt;
TOTAL                 42      2    95%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;?&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
    &lt;br /&gt;
Ejecutar el test exportado:&lt;br /&gt;
&lt;br /&gt;
Y ya puedes ejecutar el test exportado utilizando pytest:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest tests/test_selenium_ide.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto ejecutará el test generado por &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en tu navegador usando &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de Carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
# Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura el número de usuarios -por ejemplo, 10-, la tasa de generación (cada cuánto tiempo se lanza un nuevo usuario)-por ejemplo, 1- y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;). Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
# En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {&amp;quot;WebsiteTestUser&amp;quot;: 10} (10 total users)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar pruebas unitarias para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fijate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas unitarias necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1). Estas clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. En las pruebas unitarias se utilizan mocks —a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;— para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones grandes, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10103</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10103"/>
				<updated>2025-10-06T17:44:00Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de Pruebas de Software en una Aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pip install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la Aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# app/__init__.py&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella creando y visualizando las tareas usando primero el formulario web y luego también mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real; así podemos probar las rutas de la aplicación de forma rápida y controlada. &lt;br /&gt;
&lt;br /&gt;
En conjunto, este archivo hace que las pruebas sean repetibles, aisladas y automáticas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    Equivalente a preparar datos iniciales en la base de datos en UVLHUB.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
------- coverage: xxx% -------&lt;br /&gt;
&lt;br /&gt;
Name              Stmts   Miss  Cover&lt;br /&gt;
----------------  ------  ----  -----&lt;br /&gt;
app/__init__.py        1      0   100%&lt;br /&gt;
app/app.py             6      0   100%&lt;br /&gt;
app/models.py          9      0   100%&lt;br /&gt;
app/routes.py         26      2    92%&lt;br /&gt;
TOTAL                 42      2    95%&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;? &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
    &lt;br /&gt;
Ejecutar el test exportado:&lt;br /&gt;
&lt;br /&gt;
Y ya puedes ejecutar el test exportado utilizando pytest:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest tests/test_selenium_ide.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto ejecutará el test generado por &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en tu navegador usando &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de Carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
# Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura el número de usuarios -por ejemplo, 10-, la tasa de generación (cada cuánto tiempo se lanza un nuevo usuario)-por ejemplo, 1- y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;). Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
# En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {&amp;quot;WebsiteTestUser&amp;quot;: 10} (10 total users)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar pruebas unitarias para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fijate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas unitarias necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1). Estas clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. En las pruebas unitarias se utilizan mocks —a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;— para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones grandes, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10102</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10102"/>
				<updated>2025-10-06T17:42:29Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de Pruebas de Software en una Aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pip install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la Aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# app/__init__.py&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella creando y visualizando las tareas usando primero el formulario web y luego también mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real; así podemos probar las rutas de la aplicación de forma rápida y controlada. &lt;br /&gt;
&lt;br /&gt;
En conjunto, este archivo hace que las pruebas sean repetibles, aisladas y automáticas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    Equivalente a preparar datos iniciales en la base de datos en UVLHUB.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;text-align:center;&amp;quot;&lt;br /&gt;
! Name !! Stmts !! Miss !! Cover&lt;br /&gt;
|-&lt;br /&gt;
| app/__init__.py || 1 || 0 || 100%&lt;br /&gt;
|-&lt;br /&gt;
| app/app.py || 6 || 0 || 100%&lt;br /&gt;
|-&lt;br /&gt;
| app/models.py || 9 || 0 || 100%&lt;br /&gt;
|-&lt;br /&gt;
| app/routes.py || 26 || 2 || 92%&lt;br /&gt;
|-&lt;br /&gt;
! TOTAL || 42 || 2 || 95%&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;? &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
    &lt;br /&gt;
Ejecutar el test exportado:&lt;br /&gt;
&lt;br /&gt;
Y ya puedes ejecutar el test exportado utilizando pytest:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest tests/test_selenium_ide.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto ejecutará el test generado por &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en tu navegador usando &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de Carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
# Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura el número de usuarios -por ejemplo, 10-, la tasa de generación (cada cuánto tiempo se lanza un nuevo usuario)-por ejemplo, 1- y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;). Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
# En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {&amp;quot;WebsiteTestUser&amp;quot;: 10} (10 total users)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar pruebas unitarias para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fijate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas unitarias necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1). Estas clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. En las pruebas unitarias se utilizan mocks —a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;— para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones grandes, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	<entry>
		<id>https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10101</id>
		<title>Tutorial Campo de entrenamiento 2526</title>
		<link rel="alternate" type="text/html" href="https://1984.lsi.us.es/wiki-egc/index.php?title=Tutorial_Campo_de_entrenamiento_2526&amp;diff=10101"/>
				<updated>2025-10-06T17:41:44Z</updated>
		
		<summary type="html">&lt;p&gt;Jmorenol: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Automatización de Pruebas de Software en una Aplicación Flask =&lt;br /&gt;
&lt;br /&gt;
== Parte 1: creamos pruebas para una aplicación sencilla ==&lt;br /&gt;
&lt;br /&gt;
El objetivo de la primera parte de la práctica es que nos familiaricemos con diferentes tipos de pruebas usando una aplicación web desarrollada con Flask. Durante la práctica se implementarán los siguientes tipos de pruebas:&lt;br /&gt;
&lt;br /&gt;
# '''Pruebas unitarias y de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;''' para comprobar la funcionalidad interna de la aplicación y los endpoints de la API.&lt;br /&gt;
# '''Pruebas de cobertura''' para medir qué porcentaje de código está cubierto por las pruebas.&lt;br /&gt;
# '''Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt;''' para simular el comportamiento de un usuario interactuando con la interfaz web.&lt;br /&gt;
# '''Pruebas de carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt;''' para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.&lt;br /&gt;
&lt;br /&gt;
=== Dependencias ===&lt;br /&gt;
&lt;br /&gt;
Instala las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pip install flask pytest pytest-cov selenium locust webdriver-manager&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Estructura del proyecto ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
flask_testing_project/&lt;br /&gt;
│&lt;br /&gt;
├── app/&lt;br /&gt;
│   ├── __init__.py       &lt;br /&gt;
│   ├── app.py&lt;br /&gt;
│   ├── models.py&lt;br /&gt;
│   ├── routes.py&lt;br /&gt;
│   └── templates/&lt;br /&gt;
│       └── tasks.html&lt;br /&gt;
│&lt;br /&gt;
├── tests/&lt;br /&gt;
│   ├── conftest.py&lt;br /&gt;
│   ├── test_unit.py&lt;br /&gt;
│   ├── test_integration.py&lt;br /&gt;
│   └── test_interface.py.py&lt;br /&gt;
│&lt;br /&gt;
└── locustfile.py&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Desarrollo de la Aplicación Flask ===&lt;br /&gt;
&lt;br /&gt;
Vamos a crear una aplicación sencilla que gestione tareas. El usuario podrá ver las tareas en una interfaz web y agregar nuevas tareas utilizando un formulario. Además ofrece una API REST para la visualización y creación de tareas.&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/__init__.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
# app/__init__.py&lt;br /&gt;
# Indica que 'app' es un paquete Python y expone la factoría create_app.&lt;br /&gt;
&lt;br /&gt;
from .app import create_app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask&lt;br /&gt;
from app.routes import bp as tasks_blueprint&lt;br /&gt;
&lt;br /&gt;
def create_app():&lt;br /&gt;
    app = Flask(__name__)&lt;br /&gt;
    app.register_blueprint(tasks_blueprint)&lt;br /&gt;
    return app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/app.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Blueprint, jsonify, request, render_template, redirect, url_for&lt;br /&gt;
from app.models import get_all_tasks, create_task&lt;br /&gt;
&lt;br /&gt;
bp = Blueprint('tasks', __name__)&lt;br /&gt;
&lt;br /&gt;
@bp.route('/')&lt;br /&gt;
def task_list():&lt;br /&gt;
    return render_template('tasks.html', tasks=get_all_tasks())&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['GET'])&lt;br /&gt;
def get_tasks():&lt;br /&gt;
    return jsonify({'tasks': get_all_tasks()})&lt;br /&gt;
&lt;br /&gt;
@bp.route('/add_task', methods=['POST'])&lt;br /&gt;
def add_task_html():&lt;br /&gt;
    title = request.form.get('title')&lt;br /&gt;
    try:&lt;br /&gt;
        create_task(title)&lt;br /&gt;
        return redirect(url_for('tasks.task_list'))&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return str(e), 400&lt;br /&gt;
&lt;br /&gt;
@bp.route('/tasks', methods=['POST'])&lt;br /&gt;
def create_task_api():&lt;br /&gt;
    data = request.get_json()&lt;br /&gt;
    title = data.get('title') if data else None&lt;br /&gt;
    try:&lt;br /&gt;
        task = create_task(title)&lt;br /&gt;
        return jsonify(task), 201&lt;br /&gt;
    except ValueError as e:&lt;br /&gt;
        return jsonify({'error': str(e)}), 400&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Código &amp;lt;code&amp;gt;app/models.py&amp;lt;/code&amp;gt;: ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
tasks = [&lt;br /&gt;
    {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
    {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
def get_all_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Devuelve la lista de tareas.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return tasks&lt;br /&gt;
&lt;br /&gt;
def create_task(title):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea una nueva tarea con el título indicado.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if not title:&lt;br /&gt;
        raise ValueError(&amp;quot;El título es necesario&amp;quot;)&lt;br /&gt;
    new_task = {&lt;br /&gt;
        'id': tasks[-1]['id'] + 1 if tasks else 1,&lt;br /&gt;
        'title': title,&lt;br /&gt;
        'done': False&lt;br /&gt;
    }&lt;br /&gt;
    tasks.append(new_task)&lt;br /&gt;
    return new_task&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Como puedes ver, la aplicación no usa una base de datos ni un repositorio. Tan solo se usa  una lista global y un par de funciones para leer y crear tareas.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Plantilla HTML ====&lt;br /&gt;
&lt;br /&gt;
La plantilla &amp;lt;code&amp;gt;tasks.html&amp;lt;/code&amp;gt; es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;app/templates/tasks.html&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;es&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Gestor de Tareas&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;h1&amp;gt;Gestor de Tareas&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;form action=&amp;quot;{{ url_for('tasks.add_task_html') }}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Añadir nueva tarea&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Añadir tarea&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;/form&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;Lista de Tareas:&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        {% for task in tasks %}&lt;br /&gt;
            &amp;lt;li&amp;gt;{{ task.title }} {% if task.done %}(completada){% endif %}&amp;lt;/li&amp;gt;&lt;br /&gt;
        {% endfor %}&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Ejecuta la aplicación ===&lt;br /&gt;
&lt;br /&gt;
Veamos la aplicación en acción:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export FLASK_APP=app.app:create_app&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Interactúa con ella creando y visualizando las tareas usando primero el formulario web y luego también mediante la API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
curl -X POST http://127.0.0.1:5000/tasks -H &amp;quot;Content-Type: application/json&amp;quot; \&lt;br /&gt;
    -d '{&amp;quot;title&amp;quot;: &amp;quot;Leer documentación de github actions&amp;quot;}'&lt;br /&gt;
curl http://127.0.0.1:5000/tasks&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Automatización de pruebas ===&lt;br /&gt;
&lt;br /&gt;
==== Configuración del entorno de pruebas con &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
El archivo &amp;lt;code&amp;gt;conftest.py&amp;lt;/code&amp;gt; sirve para definir fixtures, que son funciones especiales que preparan el entorno antes de ejecutar los tests. En este proyecto, las fixtures garantizan que cada prueba empiece con datos limpios y una aplicación Flask lista para usarse:&lt;br /&gt;
&lt;br /&gt;
# La fixture reset_task prepara los datos iniciales antes de cada test.&lt;br /&gt;
# La fixture test_client crea un cliente de pruebas que permite simular peticiones HTTP (como GET o POST) sin necesidad de arrancar el servidor real; así podemos probar las rutas de la aplicación de forma rápida y controlada. &lt;br /&gt;
&lt;br /&gt;
En conjunto, este archivo hace que las pruebas sean repetibles, aisladas y automáticas.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import sys, os, pytest&lt;br /&gt;
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))&lt;br /&gt;
&lt;br /&gt;
from app.app import create_app&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture&lt;br /&gt;
def test_client():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Crea la aplicación Flask en modo testing y devuelve su cliente HTTP.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    app = create_app()&lt;br /&gt;
    app.testing = True&lt;br /&gt;
    return app.test_client()&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(autouse=True)&lt;br /&gt;
def reset_tasks():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture autouse (se ejecuta antes de cada test).&lt;br /&gt;
    Restablece el estado inicial de la lista de tareas.&lt;br /&gt;
    Equivalente a preparar datos iniciales en la base de datos en UVLHUB.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    models.tasks[:] = [&lt;br /&gt;
        {'id': 1, 'title': 'Comprar pan', 'done': False},&lt;br /&gt;
        {'id': 2, 'title': 'Estudiar Python', 'done': False}&lt;br /&gt;
    ]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas unitarias con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Las pruebas unitarias se centrarán en comprobar el comportamiento de funciones individuales del modelo, sin depender de Flask, HTTP ni base de datos.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_unit.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import pytest&lt;br /&gt;
from app import models&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_get_all_tasks_returns_list_of_dicts():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;get_all_tasks debe devolver una lista de tareas con formato correcto.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    result = models.get_all_tasks()&lt;br /&gt;
    assert isinstance(result, list)&lt;br /&gt;
    assert all(isinstance(t, dict) for t in result)&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in result)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_adds_new_item_and_increments_length():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;create_task debe añadir una nueva tarea y aumentar la longitud de la lista.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    initial_len = len(models.tasks)&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Aprender testing&amp;quot;)&lt;br /&gt;
    assert len(models.tasks) == initial_len + 1&lt;br /&gt;
    assert new_task in models.tasks&lt;br /&gt;
    assert new_task['title'] == &amp;quot;Aprender testing&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_increments_id_sequentially():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Los IDs de las nuevas tareas deben incrementarse de forma secuencial.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    last_id = models.tasks[-1]['id']&lt;br /&gt;
    new_task = models.create_task(&amp;quot;Nueva tarea&amp;quot;)&lt;br /&gt;
    assert new_task['id'] == last_id + 1&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_raises_value_error_if_title_missing():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Si no se pasa un título, create_task debe lanzar ValueError.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    with pytest.raises(ValueError):&lt;br /&gt;
        models.create_task(&amp;quot;&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de integración con &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas verifican que la app Flask completa funcione correctamente,&lt;br /&gt;
comprobando las rutas, peticiones y respuestas HTTP.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_integration.py&amp;lt;/code&amp;gt;: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_get_tasks_endpoint_returns_existing_tasks(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    GET /tasks debe devolver una lista JSON con las tareas iniciales.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert 'tasks' in data&lt;br /&gt;
    assert any(t['title'] == 'Comprar pan' for t in data['tasks'])&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_endpoint_returns_201_and_json(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /tasks (API JSON) debe crear una nueva tarea y devolver status 201.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={'title': 'Nueva tarea'})&lt;br /&gt;
    assert response.status_code == 201&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['title'] == 'Nueva tarea'&lt;br /&gt;
    assert 'id' in data and isinstance(data['id'], int)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_task_without_title_returns_400_error(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Si se intenta crear una tarea sin título, el servidor debe devolver error 400.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post('/tasks', json={})&lt;br /&gt;
    assert response.status_code == 400&lt;br /&gt;
&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
    assert data['error'] == 'El título es necesario'&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_html_redirects_and_renders_new_task(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    POST /add_task (formulario HTML):&lt;br /&gt;
    - debe aceptar datos enviados por formulario,&lt;br /&gt;
    - redirigir a la lista de tareas,&lt;br /&gt;
    - y mostrar la nueva tarea en el HTML.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    response = test_client.post(&lt;br /&gt;
        '/add_task',&lt;br /&gt;
        data={'title': 'Tarea desde HTML'},&lt;br /&gt;
        follow_redirects=True  # Sigue el redirect hasta la página final&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    # Comprobamos que la respuesta final es OK y contiene el título&lt;br /&gt;
    assert response.status_code == 200&lt;br /&gt;
    assert b'Tarea desde HTML' in response.data&lt;br /&gt;
    assert b'Gestor de Tareas' in response.data&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_create_then_retrieve_task_from_api(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo completo API:&lt;br /&gt;
    1. Crear una tarea con POST /tasks&lt;br /&gt;
    2. Recuperar todas las tareas con GET /tasks&lt;br /&gt;
    3. Verificar que la nueva está presente&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    test_client.post('/tasks', json={'title': 'Task persistente'})&lt;br /&gt;
    response = test_client.get('/tasks')&lt;br /&gt;
    data = response.get_json()&lt;br /&gt;
&lt;br /&gt;
    titles = [t['title'] for t in data['tasks']]&lt;br /&gt;
    assert 'Task persistente' in titles&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
De nuevo, lee el código de las pruebas e intenta deducir qué hace cada línea. En base a las pruebas manuales que has realizado antes, ¿crees que todas estas pruebas van a funcionar bien?&lt;br /&gt;
&lt;br /&gt;
==== Ejecución de las pruebas unitarias y de integración ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -v&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de cobertura con &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar &amp;lt;code&amp;gt;pytest-cov&amp;lt;/code&amp;gt;, una herramienta que extiende &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt; para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.&lt;br /&gt;
&lt;br /&gt;
Y, ¿qué es la cobertura de código?&lt;br /&gt;
&lt;br /&gt;
La cobertura de código mide el porcentaje de código ejecutado cuando se lanzan las pruebas. Esto nos ayuda a identificar las áreas de la aplicación que no están siendo probadas adecuadamente.&lt;br /&gt;
&lt;br /&gt;
===== Medir la cobertura de las pruebas con pytest-cov =====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código fue ejecutado durante las pruebas.&lt;br /&gt;
    &lt;br /&gt;
Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;text-align:center;&amp;quot;&lt;br /&gt;
! Name !! Stmts !! Miss !! Cover&lt;br /&gt;
|-&lt;br /&gt;
| app/__init__.py || 1 || 0 || 100%&lt;br /&gt;
|-&lt;br /&gt;
| app/app.py || 6 || 0 || 100%&lt;br /&gt;
|-&lt;br /&gt;
| app/models.py || 9 || 0 || 100%&lt;br /&gt;
|-&lt;br /&gt;
| app/routes.py || 26 || 2 || 92%&lt;br /&gt;
|-&lt;br /&gt;
! TOTAL || 42 || 2 || 95%&lt;br /&gt;
|}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Donde 'Stmts' indica el número total de sentencias de cada archivo; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre cada archivo.&lt;br /&gt;
&lt;br /&gt;
También se puede obtener un informe más detallado con:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest --cov=app --cov-report=html tests/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esta orden generará un informe HTML con detalles de qué líneas de código fueron ejecutadas durante las pruebas y cuáles no. El informe se generará en una carpeta llamada htmlcov/.&lt;br /&gt;
&lt;br /&gt;
Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
xdg-open htmlcov/index.html&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Con este informe podrás ver función a función cuál es su cobertura, y te permitirá identificar nuevas pruebas que añadir para verificar estos casos no cubiertos.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de interfaz con &amp;lt;code&amp;gt;Selenium&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
@pytest.fixture(scope=&amp;quot;module&amp;quot;)&lt;br /&gt;
def driver():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixture que crea y cierra automáticamente el navegador antes y después de todos los tests del módulo.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    d = initialize_driver()&lt;br /&gt;
    yield d&lt;br /&gt;
    close_driver(d)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def test_add_task_via_web_form(driver):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Flujo de prueba:&lt;br /&gt;
    1. Abrir la aplicación en http://localhost:5000/&lt;br /&gt;
    2. Escribir una nueva tarea en el formulario.&lt;br /&gt;
    3. Pulsar el botón 'Añadir tarea'.&lt;br /&gt;
    4. Comprobar que la nueva tarea aparece en la lista.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1️ Navegar a la página principal&lt;br /&gt;
    driver.get(&amp;quot;http://localhost:5000/&amp;quot;)&lt;br /&gt;
    time.sleep(1)  # pequeña espera para que la página cargue&lt;br /&gt;
&lt;br /&gt;
    # 2️ Buscar el campo de texto y escribir la tarea&lt;br /&gt;
    input_box = driver.find_element(By.NAME, &amp;quot;title&amp;quot;)&lt;br /&gt;
    input_box.clear()&lt;br /&gt;
    input_box.send_keys(&amp;quot;Tarea Selenium&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    # 3️ Enviar el formulario&lt;br /&gt;
    submit_button = driver.find_element(By.CSS_SELECTOR, &amp;quot;button[type='submit']&amp;quot;)&lt;br /&gt;
    submit_button.click()&lt;br /&gt;
    time.sleep(1)  # espera breve tras el redireccionamiento&lt;br /&gt;
&lt;br /&gt;
    # 4️ Verificar que la nueva tarea aparece en la lista&lt;br /&gt;
    page_source = driver.page_source&lt;br /&gt;
    assert &amp;quot;Tarea Selenium&amp;quot; in page_source, &amp;quot;La nueva tarea no se muestra en la lista de tareas.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?&lt;br /&gt;
&lt;br /&gt;
Pues vamos a lanzarla y comprobemos qué ocurre:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
pytest -s tests/test_interface.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo &amp;lt;code&amp;gt;tests/test_interface.py&amp;lt;/code&amp;gt;? &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Y puede que estés pensando &amp;quot;sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría bastante trabajo&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Y es cierto, pero afortunadamente existe &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, que es una herramienta fácil de usar que nos permite grabar interacciones en el navegador y convertirlas en scripts de prueba que pueden ser ejecutados automáticamente.&lt;br /&gt;
&lt;br /&gt;
===== Instalar &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Selenium IDE está disponible como una extensión para los navegadores Firefox y Chrome. Una vez instalada la extensión, haz clic en el ícono de &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en la barra de herramientas del navegador para abrirla.&lt;br /&gt;
&lt;br /&gt;
===== Grabar una prueba con &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Iniciar una nueva grabación:&lt;br /&gt;
&lt;br /&gt;
* Abre &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona &amp;lt;code&amp;gt;Create a new project&amp;lt;/code&amp;gt; y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.&lt;br /&gt;
&lt;br /&gt;
* Introduce la URL de la aplicación Flask en ejecución.&lt;br /&gt;
&lt;br /&gt;
Grabar la interacción:&lt;br /&gt;
&lt;br /&gt;
* Haz clic en el botón de grabación en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Acción 1: Abre la página principal de la aplicación Flask.&lt;br /&gt;
&lt;br /&gt;
* Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, &amp;quot;Tarea de Selenium IDE&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
* Acción 3: Haz clic en el botón para añadir la tarea.&lt;br /&gt;
&lt;br /&gt;
* Acción 4: Verifica que la nueva tarea aparece en la lista.&lt;br /&gt;
&lt;br /&gt;
* Detén la grabación una vez que hayas completado estos pasos.&lt;br /&gt;
&lt;br /&gt;
Guardar la prueba en &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===== Ejecutar la prueba grabada =====&lt;br /&gt;
&lt;br /&gt;
En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona la prueba grabada y haz clic en &amp;lt;code&amp;gt;Run current test&amp;lt;/code&amp;gt;.&lt;br /&gt;
Observa cómo &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).&lt;br /&gt;
&lt;br /&gt;
===== Exportar el test a código &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
Exportar a Python:&lt;br /&gt;
&lt;br /&gt;
* En &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt;, selecciona el menú &amp;lt;code&amp;gt;Export&amp;lt;/code&amp;gt; y elige &amp;lt;code&amp;gt;Python - pytest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.&lt;br /&gt;
    &lt;br /&gt;
Ejecutar el test exportado:&lt;br /&gt;
&lt;br /&gt;
Y ya puedes ejecutar el test exportado utilizando pytest:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ pytest tests/test_selenium_ide.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Esto ejecutará el test generado por &amp;lt;code&amp;gt;Selenium IDE&amp;lt;/code&amp;gt; en tu navegador usando &amp;lt;code&amp;gt;Selenium WebDriver&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Pruebas de Carga con &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.&lt;br /&gt;
&lt;br /&gt;
Archivo &amp;lt;code&amp;gt;locustfile.py&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from locust import HttpUser, task, between&lt;br /&gt;
&lt;br /&gt;
class WebsiteTestUser(HttpUser):&lt;br /&gt;
    wait_time = between(1, 5)&lt;br /&gt;
&lt;br /&gt;
    @task(2)&lt;br /&gt;
    def load_tasks(self):&lt;br /&gt;
        print(&amp;quot;Cargando la lista de tareas...&amp;quot;)&lt;br /&gt;
        response = self.client.get(&amp;quot;/tasks&amp;quot;)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;Lista de tareas cargada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al cargar la lista de tareas: {response.status_code}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    @task(1)&lt;br /&gt;
    def create_task(self):&lt;br /&gt;
        print(&amp;quot;Creando una nueva tarea...&amp;quot;)&lt;br /&gt;
        response = self.client.post(&amp;quot;/tasks&amp;quot;, json={&amp;quot;title&amp;quot;: &amp;quot;Tarea generada por Locust&amp;quot;})&lt;br /&gt;
        if response.status_code == 201:&lt;br /&gt;
            print(&amp;quot;Tarea creada correctamente.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Error al crear la tarea: {response.status_code}&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===== Ejecución de &amp;lt;code&amp;gt;Locust&amp;lt;/code&amp;gt; =====&lt;br /&gt;
&lt;br /&gt;
# Inicia la aplicación Flask si no estaba en ejecución:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
flask run&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Inicia Locust:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$ locust -f locustfile.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Abre la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) y configura el número de usuarios -por ejemplo, 10-, la tasa de generación (cada cuánto tiempo se lanza un nuevo usuario)-por ejemplo, 1- y el host sobre el que realizar las pruebas (&amp;lt;code&amp;gt;http://localhost:5000&amp;lt;/code&amp;gt;). Luego, inicia la prueba.&lt;br /&gt;
&lt;br /&gt;
# En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Cargando la lista de tareas...&lt;br /&gt;
Lista de tareas cargada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
Creando una nueva tarea...&lt;br /&gt;
Tarea creada correctamente.&lt;br /&gt;
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {&amp;quot;WebsiteTestUser&amp;quot;: 10} (10 total users)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Y, además, en la interfaz de Locust en tu navegador (&amp;lt;code&amp;gt;http://localhost:8089&amp;lt;/code&amp;gt;) puedes navegar por un informe interactivo con los resultados.&lt;br /&gt;
&lt;br /&gt;
¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?&lt;br /&gt;
&lt;br /&gt;
== Parte 2: Creamos pruebas para nuestra aplicación UVLHUB ==&lt;br /&gt;
&lt;br /&gt;
Con lo que hemos aprendido hasta ahora, ya tenemos las herramientas necesarias para poder diseñar pruebas automatizadas para nuestra aplicación UVLHUB. Por ejemplo, ¿qué tipo de pruebas podrías desarrollar para probar la nueva funcionalidad de notepads que creaste en la primera práctica del curso?&lt;br /&gt;
&lt;br /&gt;
No obstante, antes de lanzarte a ello quizá pueda ser buena idea echar un ojo a las pruebas ya existentes en el repositorio, que te pueden dar pistas, ideas y estrategias para diseñar las tuyas. Y al hacerlo verás que en UVLHUB se usa &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, que facilita todavía más las tareas de testing: &amp;lt;code&amp;gt;https://docs.uvlhub.io/rosemary/testing&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Pero no te agobies por tener que aprender ahora algo nuevo como &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt;, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas &amp;lt;code&amp;gt;rosemary&amp;lt;/code&amp;gt; hace llamadas a &amp;lt;code&amp;gt;pytest&amp;lt;/code&amp;gt;. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil. &lt;br /&gt;
&lt;br /&gt;
=== Un ejemplo sencillo para ayudarte a arrancar ===&lt;br /&gt;
&lt;br /&gt;
Vamos a desarrollar pruebas unitarias para el módulo notepad que preparamos en la Práctica 1 del curso. Pero en lugar de hacerlo desde cero, vamos a ver si podemos inspirarnos con algunos de los tests ya creados en el repositorio. Por ejemplo, echa un ojo a los tests del módulo profile: &amp;lt;code&amp;gt;https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Fijate bien en la función &amp;lt;code&amp;gt;test_edit_profile_page_get&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Imagina que ahora quisiéramos crear un test para comprobar que el usuario de pruebas puede solicitar la lista de sus notepads, que inicialmente está vacía. ¿Podrías inspirarte en los tests del módulo profile para crear este otro? Sería muy similar, ¿verdad?&lt;br /&gt;
&lt;br /&gt;
En el caso del notepad habría que hacer una petición get a &amp;lt;code&amp;gt;/notepad&amp;lt;/code&amp;gt;, luego habría que comprobar que el código de respuesta es 200 y finalmente comprobar que en los datos de la respuesta aparece el texto &amp;quot;You have no notepads.&amp;quot; Algo así, por ejemplo:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
def test_list_empty_notepad_get(test_client):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Tests access to the empty notepad list via GET request.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    login_response = login(test_client, &amp;quot;user@example.com&amp;quot;, &amp;quot;test1234&amp;quot;)&lt;br /&gt;
    assert login_response.status_code == 200, &amp;quot;Login was unsuccessful.&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    response = test_client.get(&amp;quot;/notepad&amp;quot;)&lt;br /&gt;
    assert response.status_code == 200, &amp;quot;The notepad page could not be accessed.&amp;quot;&lt;br /&gt;
    assert b&amp;quot;You have no notepads.&amp;quot; in response.data, &amp;quot;The expected content is not present on the page&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    logout(test_client)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Algunas cuestiones que puedes investigar ===&lt;br /&gt;
&lt;br /&gt;
Partiendo de este ejemplo anterior, seguro que podrías ir diseñando las pruebas unitarias necesarias para comprobar todas las operaciones CRUD del módulo notepad. Pero si has echado un ojo a otras pruebas de diferentes módulos de UVLHUB quizá te hayas encontrado con algunas cosas que no hemos visto en este campo de entrenamiento y que te pueden resultar muy útiles.&lt;br /&gt;
&lt;br /&gt;
Por ejemplo, en UVLHUB se usan clases de servicio (como &amp;lt;code&amp;gt;NotepadService&amp;lt;/code&amp;gt;, que creaste en la práctica 1). Estas clases encapsulan la lógica de negocio y se apoyan en un repositorio para acceder a la base de datos. En las pruebas unitarias se utilizan mocks —a través de &amp;lt;code&amp;gt;unittest.mock.patch&amp;lt;/code&amp;gt; y &amp;lt;code&amp;gt;MagicMock&amp;lt;/code&amp;gt;— para simular el comportamiento del repositorio y así probar el servicio de forma aislada, sin tocar datos reales. Este patrón es muy común en aplicaciones grandes, ya que facilita mantener las pruebas rápidas, independientes y centradas en una sola capa de la aplicación.&lt;br /&gt;
&lt;br /&gt;
También verás que las fixtures de UVLHUB son más sofisticadas: además de crear un test_client, preparan datos persistentes en la base de datos usando &amp;lt;code&amp;gt;SQLAlchemy&amp;lt;/code&amp;gt;, e incluso gestionan la sesión de usuario mediante &amp;lt;code&amp;gt;Flask-Login&amp;lt;/code&amp;gt;. Esto permite probar funcionalidades que dependen del contexto del usuario autenticado, algo que no aparecía en la app de tareas de la primera parte de esta práctica. &lt;br /&gt;
&lt;br /&gt;
Básicamente, estas técnicas amplían los fundamentos que ya has aprendido: la estructura y filosofía de las pruebas es la misma, pero ahora se aplican en un entorno más realista y con dependencias que reflejan la complejidad de una aplicación web completa.&lt;br /&gt;
&lt;br /&gt;
¡Mucho ánimo!&lt;/div&gt;</summary>
		<author><name>Jmorenol</name></author>	</entry>

	</feed>