Ejercicio

Completado

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

  1. 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. Devuelve True los valores verdaderos reconocidos, devuelve False los valores falsos reconocidos y genera un ValueError para cualquier otro valor.

  2. En el mismo archivo, anexe las pruebas para la función str_to_bool(). Use @pytest.mark.parametrize para 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 True
    
  3. A 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 False
    

    Ahora hay dos funciones de prueba que abarcan entradas representativas tanto para los valores devueltos de True como de False.

  4. 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 cuando str_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 match argumento 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

  1. Anexe una prueba basada en clases al archivo test_advanced.py . Esta prueba utiliza métodos de estilo xUnit setup_method() y teardown_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-xdist o superpuestas ejecuciones locales, este enfoque puede dejar archivos obsoletos, sobrescribir un archivo existente denominado hecho o hacer que las pruebas se afecten entre sí.

  2. Agregue un accesorio de nivel de módulo que use el accesorio tmp_path integrado 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 parametrizadas str_to_bool y antes de la clase TestFile. 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 write
    

    El accesorio tmp_file() utiliza el accesorio tmp_path de pytest, que proporciona un directorio temporal pathlib.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.

  3. Elimine la clase anterior TestFile , incluidos sus setup_method() métodos y teardown_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 Path importación que agregó en el paso anterior, ya que la prueba ya no crea un Path objeto 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 prueba True valores, una que prueba False valores y una que comprueba que los valores no válidos generan ValueError.
  • Un accesorio de pytest personalizado que usa el accesorio tmp_path integrado para crear un archivo realizado temporal con algún contenido.
  • Una sola TestFile clase con un método que usa el accesorio personalizado tmp_file para crear y leer el archivo.
  • Sin from pathlib import Path importación ni métodos setup_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.