Índice:
- Sección A. Estructurar un proyecto: apartado I, apartado II, apartado III, apartado IV, apartado V, apartado VI, apartado VII, apartado VIII.
En esta segunda parte se abordará la preparación de los requerimientos de una aplicación Python básica, así como los tests.
Dependencias básicas
Lo primero será crear el archivo requirements.txt
donde iremos incluyendo aquellas bibliotecas necesarias para el desarrollo de la aplicación:
# requirements.txt
pydantic-settings
pydantic
typing-extensions
python-dotenv
Estas extensiones son necesarias para manejar los settings.
El siguiente paso es preparar el entorno para tener tests. Hay que tener en cuenta que las bibliotecas de test no deben estar incluidas en la aplicación, ya que solo forman parte de las herramientas del desarrollador.
Por ello, en Python como en otros lenguajes, tenemos la opción de separar dichas bibliotecas. Vamos a crear el archivo requirements-dev.txt
donde iremos poniendo las correspondientes bibliotecas para los tests:
# requirements-dev.txt
# --- pytest ---
pytest-mock
pytest-asyncio
pytest-cov
coverage
pytest
typing-inspection
# --- Herramientas de calidad y formateo de código ---
flake8
black
isort
mypy
Ahora ya podemos instalar las bibliotecas:
pip install -r requirements.txt -r requirements-dev.txt
Es momento de crear la estructura necesaria:
la_fragua
├── app
│ ├── settings
│ │ └── __init__.py
│ └── __init__.py
├── tests
│ ├── settings
│ │ └── __init__.py
│ └── __init__.py
├── __init__.py
...
Tests
Una vez preparada la estructura del proyecto ya podemos empezar.
Lo primero será configurar 'pytest' para acomodarlo a nuestras preferencias:
Creamos el archivo pytest.ini
en la raíz del proyecto:
# pytest.ini
[pytest]
pythonpath = .
asyncio_default_fixture_loop_scope = function
# Configuración general
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
# Configuración de salida
addopts =
-x
--verbose
--cov=app
--cov-report=term-missing
--asyncio-mode=auto
console_output_style = progress
Ahora creamos el fichero 'conftest' cuya responsabilidad es tener las fixtures, así como cualquier necesidad común de los tests:
# tests/conftest.py
import os
import pytest
os.environ["TEST_MODE"] = "1"
Preparamos el primer test:
# tests/test_settings.py
import os
import sys
import pathlib
from app.settings.settings import settings
def test_settings_loaded_correctly():
"""
Test inicial: verificar que se lee correctamente las variables de entorno.
"""
assert settings.app_name == '(sin datos)'
assert settings.app_label == '(sin datos)'
Acto seguido lanzamos los tests:
pytest
Esta es la forma correcta de actuar, primero lanzamos el test que estará en rojo (no validado y con errores) y posteriormente empezamos a desarrollar para que el test pase en verde (validado).
En este test podemos diferenciar dos partes.
- La primera: importar las bibliotecas y archivos de desarrollo que vamos a testear.
- La segunda: la función
test_*
que contiene los pasos necesarios para validar el código.
Empecemos a programar
Vamos a crear el archivo 'settings':
# app/settings/settings.py
import os
import sys
import pathlib
class Settings:
"""
Configuración general de la aplicación.
"""
# Variables personalizables (datos por defecto)
app_name: str = '(sin datos)'
app_label: str = '(sin datos)'
settings = Settings()
Es hora de lanzar los tests. Deberían pasar en verde.
Mejorar la aplicación y el test
Muchas veces te encontrarás en la dicotomía de tener que preparar un test y programar para un paso intermedio (por falta de información, porque la aplicación no ha evolucionado lo suficiente, o simplemente porque es un paso intermedio para conseguir un objetivo), esto es normal y bueno: no intentes preparar un test que vaya directamente al punto final de una acción. Lo que tienes que tener en cuenta es que tanto los tests como el código van a ir evolucionando.
En este primer binomio de test/código vamos a tener que evolucionar. Vayamos a ello.
Primero debes corregir el test cambiando el assert
para que se valide con el contenido de las variables del futuro archivo .env
, para ello actualizamos los assert
con:
assert settings.app_name == 'la_fragua'
assert settings.app_label == 'La fragua'
Volvemos a lanzar los tests y deberían fallar: el contenido de la variable app_name
todavía es '(sin datos)', y su contenido debería ser 'la_fragua'.
El siguiente paso es crear el archivo .env
:
# .env
APP_NAME="la_fragua"
APP_LABEL="La Fragua"
Lanzamos el test y todavía estará en rojo. Necesitamos conseguir que los settings lean el archivo .env
.
Actualizamos el archivo 'settings':
# app/settings/settings.py
import os
import sys
import pathlib
from pydantic_settings import BaseSettings, SettingsConfigDict
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
base_path = pathlib.Path(sys._MEIPASS)
else:
base_path = pathlib.Path(os.getcwd()).resolve()
env_path = base_path / '.env'
class Settings(BaseSettings):
"""
Configuración general de la aplicación.
"""
# Variables personalizables (datos por defecto)
app_name: str = '(sin datos)'
app_label: str = '(sin datos)'
# Leer la configuración del fichero .env
model_config = SettingsConfigDict(
env_file=str(env_path),
env_file_encoding='utf-8',
extra='ignore'
)
settings = Settings()
Aquí podemos apreciar que añadido varias cosas:
- Con la función
SettingsConfigDict()
sobreescrimos las variables que existen con las del fichero.env
. - Un hack para que nuestra aplicación pueda resolver el path tanto en Linux como en Windows:
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
base_path = pathlib.Path(sys._MEIPASS)
else:
base_path = pathlib.Path(os.getcwd()).resolve()
env_path = base_path / '.env'
El test debería estar así:
# tests/test_settings.py
import os
import sys
import pathlib
from app.settings.settings import settings
# Test inicial de comprobación del archivo .env
def test_settings_loaded_correctly():
"""
Test inicial: verificar que se lee correctamente el archivo .env.
"""
assert settings.app_name == 'la_fragua'
assert settings.app_label == 'La Fragua'
Lanzamos los tests y pasarán en verde.
Apuntes
Como ya sabes, en Python, cada carpeta que quieras que funcione como un módulo debe tener su correspondiente fichero __init__.py
. Este paso no lo volveremos a indicar a lo largo de los diferentes artículos, por lo que deberás tenerlo presente.
En cuanto a los tests, la forma común de trabajar es usando la metodología 'Arrange/Act/Assert':
- Arrange: Preparamos los datos o funciones necesarias.
- Act: Realizamos las acciones pertinentes (lanzar una función, establecer contacto con una API, realizar una operación, etc.).
- Assert: Comprobamos que el resultado es el esperado.
Comúnmente se suele separar visualmente cada uno de los tres grupos dentro del test. Es posible que no exista un Arrange o Act si el test no necesita de una preparación o acción para pasar la validación (Assert) en verde.
Optamos por utilizar pytest
frente a otros frameworks de test por ser más simple y versátil con las fixtures.
Enlaces
Repositorio del proyecto:
Enlaces de interés:
Siguiente artículo:
Top comments (0)