Ejercicio

Completado

En este ejercicio, se usa Pytest para probar una función. A continuación, encontrará y corregirá algunos posibles problemas con la función que provocan que las pruebas tengan errores. Examinar los errores y usar los informes de errores enriquecidos de Pytest es esencial para identificar y corregir errores o pruebas problemáticas en el código de producción.

En este ejercicio, se usa una función denominada admin_command() que acepta un comando del sistema como entrada y, opcionalmente, le agrega un prefijo con la herramienta sudo. La función tiene un error que detecta escribiendo pruebas.

Paso 1: Adición de un archivo con pruebas para este ejercicio

  1. Cree un nuevo archivo de prueba usando las convenciones de nombre de archivo de Python para los archivos de prueba. Dele el nombre test_exercise.py al archivo de prueba y agregue el siguiente código:

    def admin_command(command, sudo=True):
        """
        Prefix a command with `sudo` unless it is explicitly not needed. Expects
        `command` to be a list.
        """
        if sudo:
            ["sudo"] + command
        return command
    

    La función admin_command() toma una lista como entrada mediante el argumento command y, opcionalmente, puede agregar un prefijo a la lista con sudo. Si el argumento de palabra clave sudo se establece en False, devuelve el mismo comando especificado como entrada.

  2. En el mismo archivo, anexe las pruebas para la función admin_command(). Las pruebas usan un método auxiliar que devuelve un comando de ejemplo:

    class TestAdminCommand:
    
    def command(self):
        return ["ps", "aux"]
    
    def test_no_sudo(self):
        result = admin_command(self.command(), sudo=False)
        assert result == self.command()
    
    def test_sudo(self):
        result = admin_command(self.command(), sudo=True)
        expected = ["sudo"] + self.command()
        assert result == expected
    

Nota

No es habitual tener pruebas dentro del mismo archivo como código real. Por motivos de simplicidad, los ejemplos de este ejercicio tienen código real en el mismo archivo. En los proyectos de Python reales, las pruebas suelen estar separadas por archivos y directorios del código que están probando.

Paso 2: Ejecución de las pruebas e identificación del error

Ahora que el archivo de prueba tiene una función para probarla y un par de pruebas para comprobar su comportamiento, es el momento de ejecutar las pruebas y trabajar con errores.

  • Ejecute el archivo con Python:

    $ pytest test_exercise.py
    

    La ejecución debe completarse con la superación de una prueba y un error, y la salida del error debe ser similar a la siguiente:

    =================================== FAILURES ===================================
    __________________________ TestAdminCommand.test_sudo __________________________
    
    self = <test_exercise.TestAdminCommand object at 0x10634c2e0>
    
        def test_sudo(self):
            result = admin_command(self.command(), sudo=True)
            expected = ["sudo"] + self.command()
    >       assert result == expected
    E       AssertionError: assert ['ps', 'aux'] == ['sudo', 'ps', 'aux']
    E         At index 0 diff: 'ps' != 'sudo'
    E         Right contains one more item: 'aux'
    E         Use -v to get the full diff
    
    test_exercise.py:24: AssertionError
    =========================== short test summary info ============================
    FAILED test_exercise.py::TestAdminCommand::test_sudo - AssertionError: assert...
    ========================= 1 failed, 1 passed in 0.04s ==========================
    

    El resultado produce un error en la prueba test_sudo(). Pytest proporciona detalles sobre las dos listas que se comparan. En este caso, la variable result no contiene el comando sudo, que es lo que espera la prueba.

Paso 3: Corrección del error y superación de las pruebas

Antes de realizar cambios, debe comprender por qué hay un error en primer lugar. Aunque se puede ver que no se cumple la expectativa (sudo no está en el resultado), debe averiguar por qué.

Examine las siguientes líneas de código de la función admin_command() cuando se cumpla la condición sudo=True:

    if sudo:
        ["sudo"] + command

La operación de las listas no se usa para devolver el valor. Puesto que no se devuelve, la función termina devolviendo siempre el comando sin sudo.

  1. Actualice la función admin_command() para que devuelva la operación de lista de modo que se use el resultado modificado al solicitar un comando sudo. La función actualizada debe tener este aspecto:

    def admin_command(command, sudo=True):
        """
        Prefix a command with `sudo` unless it is explicitly not needed. Expects
        `command` to be a list.
        """
        if sudo:
            return ["sudo"] + command
        return command
    
  2. Vuelva a ejecutar la prueba con Pytest. Intente aumentar el nivel de detalle de la salida mediante la marca -v con Pytest:

    $ pytest -v test_exercise.py
    
  3. Compruebe la salida. Ahora debería mostrar que se superan dos pruebas:

    ============================= test session starts ==============================
    Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 
    cachedir: .pytest_cache
    rootdir: /private
    collected 2 items
    
    test_exercise.py::TestAdminCommand::test_no_sudo PASSED                  [ 50%]
    test_exercise.py::TestAdminCommand::test_sudo PASSED                     [100%]
    
    ============================== 2 passed in 0.00s ===============================
    

Nota

Dado que la función puede trabajar con más valores de mayúsculas y minúsculas diferentes, se deben agregar más pruebas para cubrir esas variaciones. Esto impediría que los cambios futuros que se realicen en la función provoquen un comportamiento diferente (inesperado).

Paso 4: Adición de código nuevo con pruebas

Después de agregar pruebas en los pasos anteriores, debe sentirse cómodo al realizar más cambios en la función y comprobarlos con pruebas. Incluso si los cambios no están cubiertos por las pruebas existentes, puede sentirse seguro de que no está rompiendo ninguna suposición anterior.

En este caso, la función admin_command() confía ciegamente en que el argumento command siempre es una lista. Vamos a mejorarlo asegurándose de que se genera una excepción con un mensaje de error útil.

  1. En primer lugar, cree una prueba que capture el comportamiento. Aunque la función aún no se ha actualizado, pruebe un enfoque primero de prueba (también conocido como Desarrollo controlado por pruebas o TDD).

    • Actualice el archivo test_exercise.py para que importe pytest en la parte superior. Esta prueba usa un asistente interno del marco de trabajo pytest:
    import pytest
    
    • Ahora, anexe una nueva prueba a la clase para comprobar la excepción. Esta prueba debe esperar un TypeError de la función cuando el valor que se le pasa no es una lista:
        def test_non_list_commands(self):
            with pytest.raises(TypeError):
                admin_command("some command", sudo=True)
    
  2. Vuelva a ejecutar las pruebas con Pytest. Todos deberían pasar:

    ============================= test session starts ==============================
    Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
    rootdir: /private/
    collected 3 items
    
    test_exercise.py ...                                                     [100%]
    
    ============================== 3 passed in 0.00s ===============================
    

    La prueba es lo suficientemente buena para comprobar TypeError, pero sería bueno agregar el código con un mensaje de error útil.

  3. Actualice la función para generar explícitamente un TypeError con un mensaje de error útil:

    def admin_command(command, sudo=True):
        """
        Prefix a command with `sudo` unless it is explicitly not needed. Expects
        `command` to be a list.
        """
        if not isinstance(command, list):
            raise TypeError(f"was expecting command to be a list, but got a {type(command)}")
        if sudo:
            return ["sudo"] + command
        return command
    
  4. Por último, actualice el método test_non_list_commands() para comprobar el mensaje de error:

    def test_non_list_commands(self):
        with pytest.raises(TypeError) as error:
            admin_command("some command", sudo=True)
        assert error.value.args[0] == "was expecting command to be a list, but got a <class 'str'>"
    

    La prueba actualizada usa error como una variable que contiene toda la información de excepción. Con error.value.args, puede examinar los argumentos de la excepción. En este caso, el primer argumento tiene la cadena de error que la prueba puede comprobar.

Compruebe su trabajo

Llegado a este punto, debe tener un archivo de prueba de Python llamado test_exercise.py, en el que se incluya lo siguiente:

  • Una función admin_command() que acepta un argumento y un argumento de palabra clave.
  • Una excepción TypeError con un mensaje de error útil en la función admin_command().
  • Una clase de prueba TestAdminCommand() que tiene un método auxiliar command() y tres métodos de prueba que comprueban la función admin_command().

Todas las pruebas deben pasar sin errores al ejecutarlas en el terminal.