Ejercicio
En este ejercicio, usará pytest con @pytest.mark.parametrize para probar una función, incluidos los casos que esperan una excepción. A continuación, refactorizará una prueba basada en clases, pasando del enfoque setup_method() y teardown_method() de estilo xUnit a un accesorio. El uso de parametrización y accesorios le ayuda a escribir pruebas que cubren más casos con menos repetición y configuración más clara.
Para obtener ayuda para la instalación, consulte la documentación de Python para entornos virtuales, la documentación de pip para pip install y la documentación de pytest para installing pytest. Para más información sobre las características pytest de este ejercicio, consulte la documentación de pytest para parametrizar funciones de prueba, aserciones sobre excepciones esperadas, accesorios, configuración de estilo xUnit y directorios y archivos temporales.
Antes de comenzar
Use Python 3.10 o posterior, como se describe en los requisitos previos del módulo. Si ya tiene un entorno virtual con pytest instalado y activado, continúe con el paso 1. De lo contrario, cree y active un entorno virtual en la carpeta donde creará test_advanced.py.
En Windows PowerShell:
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install pytest
python -m pytest --version
En macOS, Linux o Subsistema de Windows para Linux:
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install pytest
python -m pytest --version
El comando version debe imprimir una versión pytest, similar a esta salida:
pytest x.y.z
Los comandos de instalación no anclan pytest a una versión específica. Pip usa metadatos de paquete para elegir una versión pytest estable compatible con la versión de Python en el entorno virtual.
Use python -m pytest en este ejercicio para que pytest se ejecute desde el entorno activo. Si Windows PowerShell bloquea Activate.ps1, consulte la documentación de Python venv para la opción de directiva de ejecución de PowerShell o ejecute el Python del entorno virtual directamente para los comandos pip y pytest. Por ejemplo:
.\.venv\Scripts\python -m pip install --upgrade pip
.\.venv\Scripts\python -m pip install pytest
.\.venv\Scripts\python -m pytest -v test_advanced.py
Paso 1: Adición de un archivo con pruebas para este ejercicio
En la misma carpeta donde ejecutará pytest, cree un nuevo archivo de prueba denominado test_advanced.py. No coloque el archivo dentro de la carpeta .venv . Agrega el código siguiente:
import pytest def str_to_bool(string): normalized = string.lower() if normalized in ["yes", "y", "1"]: return True if normalized in ["no", "n", "0"]: return False raise ValueError(f"Cannot convert {string!r} to a boolean")La función
str_to_bool()acepta una cadena como entrada. DevuelveTruelos valores verdaderos reconocidos, devuelveFalselos valores falsos reconocidos y genera unValueErrorpara cualquier otro valor.En el mismo archivo, anexe las pruebas para la función
str_to_bool(). Use@pytest.mark.parametrizepara probar primero los valores true:@pytest.mark.parametrize("string", ["Y", "y", "1", "YES"]) def test_str_to_bool_true(string): assert str_to_bool(string) is TrueA continuación, anexe otra prueba con los valores false:
@pytest.mark.parametrize("string", ["N", "n", "0", "NO"]) def test_str_to_bool_false(string): assert str_to_bool(string) is FalseAhora hay dos funciones de prueba que abarcan entradas representativas tanto para los valores devueltos de
Truecomo deFalse.Por último, anexe una prueba parametrizada para los valores que la función no debe aceptar. Use
pytest.raises()como administrador de contextos para que la prueba se supere solo cuandostr_to_bool()genera la excepción esperada:@pytest.mark.parametrize("string", ["maybe", "2"]) def test_str_to_bool_invalid(string): with pytest.raises(ValueError, match="Cannot convert"): str_to_bool(string)El
matchargumento comprueba el mensaje de excepción con una expresión regular. Aquí, la prueba solo comprueba la parte estable del mensaje para que no dependa del valor no válido exacto.
Nota
Para simplificar, este ejercicio mantiene el código sometido a prueba y las pruebas en el mismo archivo. En proyectos de Python reales, el código de aplicación y las pruebas normalmente se separan en diferentes archivos y directorios, como un paquete />
Paso 2: Ejecución de las pruebas y exploración del informe
Después de agregar las pruebas, ejecute pytest e inspeccione la salida. Utilice la bandera de verbosidad aumentada (-v) para que pueda ver los valores de entrada tratados como pruebas independientes.
En Windows PowerShell:
python -m pytest -v test_advanced.py
En macOS, Linux o Subsistema de Windows para Linux:
python -m pytest -v test_advanced.py
La salida debe ser similar al informe siguiente:
============================= test session starts ==============================
platform ... -- Python 3.x.y, pytest-x.y.z, pluggy-x.y.z -- ...
rootdir: ...
...
collected 10 items
test_advanced.py::test_str_to_bool_true[Y] PASSED [ 10%]
test_advanced.py::test_str_to_bool_true[y] PASSED [ 20%]
test_advanced.py::test_str_to_bool_true[1] PASSED [ 30%]
test_advanced.py::test_str_to_bool_true[YES] PASSED [ 40%]
test_advanced.py::test_str_to_bool_false[N] PASSED [ 50%]
test_advanced.py::test_str_to_bool_false[n] PASSED [ 60%]
test_advanced.py::test_str_to_bool_false[0] PASSED [ 70%]
test_advanced.py::test_str_to_bool_false[NO] PASSED [ 80%]
test_advanced.py::test_str_to_bool_invalid[maybe] PASSED [ 90%]
test_advanced.py::test_str_to_bool_invalid[2] PASSED [100%]
============================== 10 passed in 0.01s ==============================
Su sistema operativo, la versión de Python, la versión de pytest, la ruta del ejecutable, el directorio raíz, las líneas de complementos y caché, el formato del progreso de la recopilación, el ajuste de saltos de línea y el tiempo de ejecución pueden diferir. Aunque solo escribió tres funciones de prueba, pytest recopiló 10 pruebas porque cada valor de entrada parametrizado se ejecuta como su propio caso de prueba.
Paso 3 - Refactorizar la instalación y el desmontaje en un accesorio
Anexe una prueba basada en clases al archivo test_advanced.py . Esta prueba utiliza métodos de estilo xUnit
setup_method()yteardown_method()para crear y eliminar un archivo en torno a cada método de prueba.from pathlib import Path class TestFile: def setup_method(self): self.path = Path("done") self.path.write_text("1", encoding="utf-8") def teardown_method(self): self.path.unlink(missing_ok=True) def test_done_file(self): contents = self.path.read_text(encoding="utf-8") assert contents == "1"Esta prueba puede superarse, pero escribe un nombre de archivo fijo en el directorio de trabajo actual y requiere código de limpieza independiente. Si se interrumpe una ejecución de pruebas o las pruebas se ejecutan simultáneamente, como con
pytest-xdisto superpuestas ejecuciones locales, este enfoque puede dejar archivos obsoletos, sobrescribir un archivo existente denominado hecho o hacer que las pruebas se afecten entre sí.Agregue un accesorio de nivel de módulo que use el accesorio
tmp_pathintegrado para crear un archivo temporal. En el paso siguiente, actualizará la clase para usar este accesorio en lugar de los métodos instalación y desmontaje. Para facilitar la lectura, coloque este accesorio a nivel de módulo después de las pruebas parametrizadasstr_to_booly antes de la claseTestFile. Pytest detecta los accesorios de nivel de módulo independientemente de dónde aparezcan dentro del módulo, por lo que la posición es para lectores humanos y no para pytest.@pytest.fixture def tmp_file(tmp_path): def write(): path = tmp_path / "done" path.write_text("1", encoding="utf-8") return path return writeEl accesorio
tmp_file()utiliza el accesoriotmp_pathde pytest, que proporciona un directorio temporalpathlib.Pathúnico para cada invocación de función de prueba, incluidos cada caso parametrizado. Pytest administra el directorio temporal, por lo que la prueba no depende de una ruta de acceso de archivo codificada de forma rígida.Elimine la clase anterior
TestFile, incluidos sussetup_method()métodos yteardown_method(), y reemplácela por esta versión final que usa el accesorio en lugar de los métodos de instalación y desmontaje:class TestFile: def test_done_file(self, tmp_file): path = tmp_file() contents = path.read_text(encoding="utf-8") assert contents == "1"También puede quitar la
from pathlib import Pathimportación que agregó en el paso anterior, ya que la prueba ya no crea unPathobjeto directamente.
Compruebe su trabajo
Por ahora debería tener un archivo Python denominado test_advanced.py que contenga:
- Función
str_to_bool()que acepta una cadena y devuelve un valor booleano para los valores true y false reconocidos. - Tres pruebas parametrizadas para la
str_to_bool()función: una que pruebaTruevalores, una que pruebaFalsevalores y una que comprueba que los valores no válidos generanValueError. - Un accesorio de pytest personalizado que usa el accesorio
tmp_pathintegrado para crear un archivo realizado temporal con algún contenido. - Una sola
TestFileclase con un método que usa el accesorio personalizadotmp_filepara crear y leer el archivo. - Sin
from pathlib import Pathimportación ni métodossetup_method()/teardown_method()en la versión final.
El archivo final debe tener un aspecto similar al siguiente:
import pytest
def str_to_bool(string):
normalized = string.lower()
if normalized in ["yes", "y", "1"]:
return True
if normalized in ["no", "n", "0"]:
return False
raise ValueError(f"Cannot convert {string!r} to a boolean")
@pytest.mark.parametrize("string", ["Y", "y", "1", "YES"])
def test_str_to_bool_true(string):
assert str_to_bool(string) is True
@pytest.mark.parametrize("string", ["N", "n", "0", "NO"])
def test_str_to_bool_false(string):
assert str_to_bool(string) is False
@pytest.mark.parametrize("string", ["maybe", "2"])
def test_str_to_bool_invalid(string):
with pytest.raises(ValueError, match="Cannot convert"):
str_to_bool(string)
@pytest.fixture
def tmp_file(tmp_path):
def write():
path = tmp_path / "done"
path.write_text("1", encoding="utf-8")
return path
return write
class TestFile:
def test_done_file(self, tmp_file):
path = tmp_file()
contents = path.read_text(encoding="utf-8")
assert contents == "1"
Vuelva a ejecutar el archivo de prueba.
En Windows PowerShell:
python -m pytest -v test_advanced.py
En macOS, Linux o Subsistema de Windows para Linux:
python -m pytest -v test_advanced.py
El informe final debe mostrar 11 pruebas recopiladas: 10 casos de las pruebas parametrizadas y una prueba basada en clases.
============================= test session starts ==============================
platform ... -- Python 3.x.y, pytest-x.y.z, pluggy-x.y.z -- ...
rootdir: ...
...
collected 11 items
test_advanced.py::test_str_to_bool_true[Y] PASSED [ 9%]
test_advanced.py::test_str_to_bool_true[y] PASSED [ 18%]
test_advanced.py::test_str_to_bool_true[1] PASSED [ 27%]
test_advanced.py::test_str_to_bool_true[YES] PASSED [ 36%]
test_advanced.py::test_str_to_bool_false[N] PASSED [ 45%]
test_advanced.py::test_str_to_bool_false[n] PASSED [ 54%]
test_advanced.py::test_str_to_bool_false[0] PASSED [ 63%]
test_advanced.py::test_str_to_bool_false[NO] PASSED [ 72%]
test_advanced.py::test_str_to_bool_invalid[maybe] PASSED [ 81%]
test_advanced.py::test_str_to_bool_invalid[2] PASSED [ 90%]
test_advanced.py::TestFile::test_done_file PASSED [100%]
============================== 11 passed in 0.01s ==============================
Todas las pruebas deben superarse sin errores.