Tutorial Campo de entrenamiento

De Wiki de EGC
Saltar a: navegación, buscar

Automatización de Pruebas de Software en una Aplicación Flask

Parte 1: creamos pruebas para una aplicación sencilla

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:

  1. Pruebas unitarias con pytest para comprobar la funcionalidad interna de la aplicación.
  2. Pruebas de cobertura para comprobar si nuestras pruebas tienen una buena cobertura de código.
  3. Pruebas de interfaz con Selenium para simular el comportamiento de un usuario interactuando con la interfaz web.
  4. Pruebas de carga con Locust para evaluar el rendimiento de la aplicación bajo diferentes niveles de tráfico.

Requisitos previos

Antes de comenzar, asegúrate de tener instalados los siguientes paquetes y herramientas:

  • Python 3
  • Flask
  • pytest para pruebas unitarias.
  • pytest-cov para la cobertura de código.
  • Selenium para pruebas de interfaz.
  • Locust para pruebas de carga.
  • El navegador chromium y chromium-driver (para Selenium).

Para instalar las dependencias necesarias (¡pero recuerda hacerlo en un entorno virtual!):

$ pip install flask pytest pytest-cov selenium locust
Estructura del proyecto
flask_testing_project/
│
├── app.py                # Archivo principal de la aplicación Flask
├── templates/            # Directorio que contiene la plantilla HTML
│   └── tasks.html        # Plantilla para mostrar y agregar tareas
├── tests/
│   ├── test_app.py       # Pruebas unitarias usando pytest
│   └── test_interfaz.py  # Pruebas de interfaz con Selenium
└── locustfile.py         # Archivo para pruebas de carga con Locust

Desarrollo de la Aplicación Flask

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.

Código app.py:
from flask import Flask, jsonify, request, render_template, redirect, url_for

app = Flask(__name__)

# Lista inicial de tareas (guardada en memoria)
tasks = [
    {'id': 1, 'title': 'Comprar pan', 'done': False},
    {'id': 2, 'title': 'Estudiar Python', 'done': False}
]

# Ruta para obtener la lista de tareas (versión HTML)
@app.route('/')
def task_list():
    return render_template('tasks.html', tasks=tasks)

# Ruta para obtener la lista de tareas en JSON (API)
@app.route('/tasks', methods=['GET'])
def get_tasks():
    return jsonify({'tasks': tasks})

# Ruta para crear una nueva tarea desde un formulario HTML
@app.route('/add_task', methods=['POST'])
def add_task_html():
    title = request.form.get('title')
    if not title:
        return "El título es necesario", 400
    task = {
        'id': tasks[-1]['id'] + 1 if tasks else 1,
        'title': title,
        'done': False
    }
    tasks.append(task)
    return redirect(url_for('task_list'))

# Ruta para crear una nueva tarea (API JSON)
@app.route('/tasks', methods=['POST'])
def create_task():
    if not request.json or 'title' not in request.json:
        return jsonify({'error': 'El título es necesario'}), 400
    task = {
        'id': tasks[-1]['id'] + 1 if tasks else 1,
        'title': request.json['title'],
        'done': False
    }
    tasks.append(task)
    return jsonify(task), 201

if __name__ == '__main__':
    app.run(debug=True)

Como puedes ver en el código, por tanto, se ofrecen dos formas para interactuar con las tareas:

  1. Una página HTML que muestra la lista de tareas y un formulario para añadir nuevas tareas.
  1. Una API REST que devuelve la lista de tareas en formato JSON y permite agregar nuevas tareas mediante solicitudes POST.
Plantilla HTML

La plantilla tasks.html es la encargada de mostrar las tareas y proporcionar un formulario para agregar nuevas tareas.

Archivo templates/tasks.html:
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Gestor de Tareas</title>
</head>
<body>
    <h1>Gestor de Tareas</h1>

    <!-- Formulario para añadir una nueva tarea -->
    <form action="{{ url_for('add_task_html') }}" method="POST">
        <input type="text" name="title" placeholder="Añadir nueva tarea">
        <button type="submit">Añadir tarea</button>
    </form>

    <!-- Lista de tareas -->
    <h2>Lista de Tareas:</h2>
    <ul>
        {% for task in tasks %}
            <li>{{ task.title }} {% if task.done %} (completada) {% endif %}</li>
        {% endfor %}
    </ul>
</body>
</html>
Lanza la aplicación

Veamos la aplicación en acción:

$ python3 app.py

Interactúa con ella creando y visualizando las tareas usando primero el formulario web y luego también mediante la API:

$ curl -X POST http://127.0.0.1:5000/tasks -H "Content-Type: application/json" \
    -d '{"title": "Leer documentación de github actions"}'
$ curl http://127.0.0.1:5000/tasks

Pruebas Unitarias con pytest

Las pruebas unitarias se centrarán en probar los endpoints de la API de manera independiente.

Archivo tests/test_app.py:
import pytest
from app import app

@pytest.fixture
def client():
    with app.test_client() as client:
        yield client

def test_get_tasks(client):
    response = client.get('/tasks')
    assert response.status_code == 200
    assert 'Comprar pan' in response.get_data(as_text=True)

def test_create_task(client):
    response = client.post('/tasks', json={'title': 'Aprender testing'})
    assert response.status_code == 201
    assert 'Aprender testing' in response.get_data(as_text=True)

def test_create_task_without_title(client):
    response = client.post('/tasks', json={})
    assert response.status_code == 400
    data = response.get_json()
    assert data['error'] == 'El título es necesario'

def test_task_list_updates(client):
    response = client.post('/tasks', json={'title': 'Otra nueva tarea'})
    assert response.status_code == 201

    response = client.get('/tasks')
    assert 'Otra nueva tarea' in response.get_data(as_text=True)

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?

Ejecución de las pruebas:
$ pytest

Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?

NOTA: Si recibes un error al lanzar pytest porque no se encuentra el módulo app, puedes intentarlo así (lo que añade el directorio actual (.) al PYTHONPATH):

$ PYTHONPATH=. pytest

Pruebas de cobertura con pytest-cov

Para asegurarnos de que nuestras pruebas unitarias tienen una buena cobertura de código, vamos a utilizar pytest-cov, una herramienta que extiende pytest para generar un informe sobre qué porcentaje del código ha sido cubierto por las pruebas.

Y, ¿qué es la cobertura de código?

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.

Medir la cobertura de las pruebas con pytest-cov
$ pytest --cov=app tests/

Este comando ejecutará todas las pruebas en la carpeta tests/ y generará un informe de cobertura que mostrará qué porcentaje del código en el archivo app.py fue ejecutado durante las pruebas.

Tras ejecutar la orden anterior deberías ver una salida del estilo de la siguiente:

------- coverage: xxx% -------

 Name      | Stmts | Miss | Cover                      
-----------| ------|------|------
 app.py    | 26    | 8    | 69%
 TOTAL     | 26    | 8    | 69%

Donde 'Stmts' indica el número total de sentencias en el archivo app.py; 'Miss' indica el número de sentencias que no fueron ejecutadas por las pruebas; y 'Cover' el porcentaje de cobertura de las pruebas sobre el archivo app.py.

También se puede obtener un informe más detallado con:

$ pytest --cov=app --cov-report=html tests/

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/.

Para visualizar el informe, abre el archivo htmlcov/index.html en tu navegador:

$ xdg-open htmlcov/index.html

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.

Pruebas de interfaz con Selenium

Estas pruebas simulan la interacción de un usuario con la interfaz web de la aplicación.

Un pasito previo para ver Selenium en acción

Antes de realizar las pruebas sobre nuestra aplicación vamos a hacer un pasito previo para comprobar que tenemos chromium y chromium-driver correctamente instalados y entender el tipo de cosas que podemos hacer con Selenium.

Archivo tests/test_selenium.py :
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

# Configurar Selenium para usar Chromium
options = webdriver.ChromeOptions()
# Quita '--headless' para ejecutar el navegador de manera visible
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')

# Iniciar el driver de Chromium
driver = webdriver.Chrome(options=options)

try:
    # 1. Abrir Google
    driver.get("https://www.google.com")

    # 2. Esperar hasta que aparezca la ventana de cookies y hacer clic en "Rechazar todo"
    reject_cookies_button = WebDriverWait(driver, 10).until(
        EC.element_to_be_clickable((By.XPATH, "//button[contains(., 'Rechazar todo')]"))
    )
    reject_cookies_button.click()

    # 3. Esperar hasta que el campo de búsqueda sea visible
    search_box = WebDriverWait(driver, 10).until(
        EC.visibility_of_element_located((By.NAME, "q"))
    )

    # 4. Escribir "Selenium" en la barra de búsqueda y enviar el formulario
    search_box.send_keys("Selenium")
    search_box.send_keys(Keys.RETURN)

    # 5. Esperar a que el título cambie y contenga "Selenium"
    WebDriverWait(driver, 10).until(EC.title_contains("Selenium"))
    print("¡Selenium está funcionando correctamente con Chromium!")

except Exception as e:
    print(f"Error: {e}")
    driver.save_screenshot("error_screenshot.png")
    print("Captura de pantalla guardada como 'error_screenshot.png'")

finally:
    # Cerrar el navegador
    driver.quit()

¿Qué crees que va a ocurrir cuando ejecutemos esta prueba?

Pues vamos a lanzarala y comprobemos qué ocurre:

$ pytest -s tests/test_selenium.py

¿Has visto cómo se ha lanzado el navegador y ha ido realizando los pasos indicados en el archivo tests/test_selenium.py? Pues ya tenemos todo listo para realizar las pruebas sobre nuestra aplicación.

Archivo tests/test_interfaz.py:

Ahora sí, vamos a crear las pruebas de la vista para nuestra aplicación.

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
import pytest

@pytest.fixture
def driver():
    print("Iniciando el navegador Chromium...")
    driver = webdriver.Chrome()
    yield driver
    print("Cerrando el navegador Chromium...")
    driver.quit()

def test_add_task(driver):
    print("Abriendo la aplicación web en localhost:5000...")
    driver.get("http://localhost:5000")

    print("Verificando que el título de la página es correcto...")
    assert "Gestor de Tareas" in driver.title

    print("Buscando el campo de entrada de nueva tarea...")
    input_field = driver.find_element(By.NAME, "title")

    print("Escribiendo 'Tarea de Selenium' en el campo de entrada...")
    input_field.send_keys("Tarea de Selenium")
    input_field.send_keys(Keys.RETURN)

    print("Verificando que 'Tarea de Selenium' aparece en la lista de tareas...")
    assert "Tarea de Selenium" in driver.page_source

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?

Ejecución de las pruebas de interfaz:
$ pytest -s tests/test_interfaz.py

Comprueba los resultados obtenidos. ¿Coinciden con lo que estabas esperando?

Selenium IDE

Y puede que estés pensando "sí, vale, las pruebas han funcionado como esperaba... pero si tuviera que escribir yo la prueba me costaría mucho trabajo".

Y es cierto, pero afortunadamente existe Selenium IDE, 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.

Instalar Selenium IDE

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 Selenium IDE en la barra de herramientas del navegador para abrirla.

Grabar una prueba con Selenium IDE

Iniciar una nueva grabación:

  • Abre Selenium IDE.
  • Selecciona Create a new project y dale un nombre a tu proyecto, por ejemplo, PruebasFlaskInterfaz.
  • Introduce la URL de la aplicación Flask en ejecución.

Grabar la interacción:

  • Haz clic en el botón de grabación en Selenium IDE.
  • Acción 1: Abre la página principal de la aplicación Flask.
  • Acción 2: En el formulario de tareas, escribe una nueva tarea, por ejemplo, "Tarea de Selenium IDE".
  • Acción 3: Haz clic en el botón para añadir la tarea.
  • Acción 4: Verifica que la nueva tarea aparece en la lista.
  • Detén la grabación una vez que hayas completado estos pasos.

Guardar la prueba en Selenium IDE.

Ejecutar la prueba grabada

En Selenium IDE, selecciona la prueba grabada y haz clic en Run current test. Observa cómo Selenium IDE reproduce automáticamente todas las acciones que realizaste durante la grabación (navegar, escribir en el formulario, etc.).

Exportar el test a código Selenium WebDriver

Exportar a Python:

  • En Selenium IDE, selecciona el menú Export y elige Python - Selenium WebDriver.
  • Selecciona la carpeta de pruebas y guárdalo como test_selenium_ide.py.

Ejecutar el test exportado:

Y ya puedes ejecutar el test exportado utilizando pytest:

$ pytest tests/test_selenium_ide.py

Esto ejecutará el test generado por Selenium IDE en tu navegador usando Selenium WebDriver.

Pruebas de Carga con Locust

Locust simulará múltiples usuarios accediendo a la aplicación simultáneamente, realizando operaciones como cargar la lista de tareas y agregar nuevas tareas.

Archivo locustfile.py:
from locust import HttpUser, task, between

class WebsiteTestUser(HttpUser):
    wait_time = between(1, 5)

    @task(2)
    def load_tasks(self):
        print("Cargando la lista de tareas...")
        response = self.client.get("/tasks")
        if response.status_code == 200:
            print("Lista de tareas cargada correctamente.")
        else:
            print(f"Error al cargar la lista de tareas: {response.status_code}")

    @task(1)
    def create_task(self):
        print("Creando una nueva tarea...")
        response = self.client.post("/tasks", json={"title": "Tarea generada por Locust"})
        if response.status_code == 201:
            print("Tarea creada correctamente.")
        else:
            print(f"Error al crear la tarea: {response.status_code}")
Ejecución de Locust
  1. Inicia la aplicación Flask si no estaba en ejecución:
$ python app.py
  1. Inicia Locust:
$ locust -f locustfile.py
  1. Abre la interfaz de Locust en tu navegador (http://localhost:8089) 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 (http://localhost:5000). Luego, inicia la prueba.
  1. En la terminal verás mensajes como estos hasta que se haya lanzado el número de clientes indicado:
Cargando la lista de tareas...
Lista de tareas cargada correctamente.
Creando una nueva tarea...
Tarea creada correctamente.
Creando una nueva tarea...
Tarea creada correctamente.
Cargando la lista de tareas...
Lista de tareas cargada correctamente.
Creando una nueva tarea...
Tarea creada correctamente.
Creando una nueva tarea...
Tarea creada correctamente.
[2024-10-07 17:35:02,798] hostname/INFO/locust.runners: All users spawned: {"WebsiteTestUser": 10} (10 total users)

Y además en la interfaz de Locust en tu navegador (http://localhost:8089) puedes navegar por un informe interactivo con los resultados.

¿Cómo han ido las pruebas? ¿Ha aguantado el sistema esta carga?

Parte 2: Creamos pruebas para nuestra aplicación UVLHUB

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?

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 rosemary, que facilita todavía más las tareas de testing: https://docs.uvlhub.io/rosemary/testing.

Pero no te agobies por tener que aprender ahora algo nuevo como rosemary, ya que si echas un ojo al código del repositorio vas a ver que, en realidad, para lanzar las pruebas rosemary hace llamadas a pytest. Su uso es totalmente opcional, aunque es cierto nos hace la vida un poquito más fácil.

Un ejemplo sencillo para ayudarte a arrancar

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: https://github.com/EGCETSII/uvlhub/blob/main/app/modules/profile/tests/test_unit.py

Fijate bien en la función test_edit_profile_page_get.

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?

En el caso del notepad habría que hacer una petición get a /notepad, 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 "You have no notepads." Algo así, por ejemplo:

def test_list_empty_notepad_get(test_client):
    """
    Tests access to the empty notepad list via GET request.
    """
    login_response = login(test_client, "user@example.com", "test1234")
    assert login_response.status_code == 200, "Login was unsuccessful."

    response = test_client.get("/notepad")
    assert response.status_code == 200, "The notepad page could not be accessed."
    assert b"You have no notepads." in response.data, "The expected content is not present on the page"

    logout(test_client)

Partiendo de este ejemplo, ¿podrías ir diseñando las pruebas unitarias necesarias para comprobar todas las operaciones CRUD del módulo notepad?

¡Mucho ánimo!