Agrupación de pruebas en pytest: clases frente a funciones simples

6 minutos de lectura

avatar de usuario
NI6

Estoy usando pytest para probar mi aplicación. pytest admite 2 enfoques (que conozco) sobre cómo escribir pruebas:

  1. En clases:

test_feature.py -> clase TestFeature -> def test_feature_sanity

  1. En funciones:

test_feature.py -> def test_feature_sanity

¿Es necesario el enfoque de agrupar las pruebas en una clase? ¿Está permitido respaldar el módulo integrado de prueba unitaria? ¿Qué enfoque diría usted que es mejor y por qué?

  • Como se menciona en pytest documentación, puede usarla para ejecutar unittest pruebas En cuanto a agrupar pruebas en una clase, es principalmente una cuestión de gusto y organización.

    –Ignacio Vergara Kausel

    25 de abril de 2018 a las 8:12

avatar de usuario
Jorge

No existen reglas estrictas con respecto a la organización de las pruebas en módulos frente a clases. Es una cuestión de preferencia personal. Inicialmente intenté organizar las pruebas en clases, después de un tiempo me di cuenta de que no necesitaba otro nivel de organización. Hoy en día solo recopilo funciones de prueba en módulos (archivos).

Pude ver un caso de uso válido cuando algunas pruebas podrían organizarse lógicamente en el mismo archivo, pero aún así tener un nivel adicional de organización en clases (por ejemplo, para hacer uso del dispositivo de ámbito de clase). Pero esto también se puede hacer simplemente dividiéndolo en múltiples módulos.

  • Me gustaría reforzar lo que dice Zim sobre el uso de accesorios de alcance de clase al proponer que esta es la razón principal para agrupar las pruebas en una clase. Si no necesita el “anidamiento de accesorios” proporcionado por tener un accesorio con alcance de clase (que opera intencionalmente dentro de sus accesorios con alcance de módulo y sesión), diría que no hay razón para tener una clase.

    – Timblaktu

    26 oct 2018 a las 16:51

avatar de usuario
jasha

Esta respuesta presenta dos casos de uso convincentes para TestClass en pytest:

  • Parametrización conjunta de múltiples métodos de ensayo pertenecientes a una clase determinada.
  • Reutilización de datos de prueba y lógica de prueba a través de herencia de subclase

Parametrización conjunta de múltiples métodos de ensayo pertenecientes a una clase dada.

El decorador de parametrización pytest, @pytest.mark.parametrize, se puede usar para hacer que las entradas estén disponibles para varios métodos dentro de una clase. En el siguiente código, las entradas param1 y param2 están disponibles para cada uno de los métodos TestGroup.test_one y TestGroup.test_two.

"""test_class_parametrization.py"""
import pytest

@pytest.mark.parametrize(
    "param1,param2",
    [
        ("a", "b"),
        ("c", "d"),
    ],
)
class TestGroup:
    """A class with common parameters, `param1` and `param2`."""

    @pytest.fixture
    def fixt(self):
        """This fixture will only be available within the scope of TestGroup"""
        return 123

    def test_one(self, param1, param2, fixt):
        print("\ntest_one", param1, param2, fixt)

    def test_two(self, param1, param2):
        print("\ntest_two", param1, param2)
$ pytest -s test_class_parametrization.py
================================================================== test session starts ==================================================================
platform linux -- Python 3.8.6, pytest-6.2.1, py-1.10.0, pluggy-0.13.1
rootdir: /home/jbss
plugins: pylint-0.18.0
collected 4 items

test_class_parametrization.py
test_one a b 123
.
test_one c d 123
.
test_two a b
.
test_two c d
.

=================================================================== 4 passed in 0.01s ===================================================================

Reutilización de datos de prueba y lógica de prueba a través de herencia de subclase

Usaré una versión modificada del código tomado de otra respuesta para demostrar la utilidad de heredar atributos/métodos de clase de TestClass a TestSubclass:

# in file `test_example.py`
class TestClass:
    VAR = 3
    DATA = 4

    def test_var_positive(self):
        assert self.VAR >= 0


class TestSubclass(TestClass):
    VAR = 8

    def test_var_even(self):
        assert self.VAR % 2 == 0

    def test_data(self):
        assert self.DATA == 4

Correr pytest en este archivo causa cuatro pruebas a ejecutar:

$ pytest -v test_example.py
=========== test session starts ===========
platform linux -- Python 3.8.2, pytest-5.4.2, py-1.8.1
collected 4 items

test_example.py::TestClass::test_var_positive PASSED
test_example.py::TestSubclass::test_var_positive PASSED
test_example.py::TestSubclass::test_var_even PASSED
test_example.py::TestSubclass::test_data PASSED

En la subclase, el heredado test_var_positive el método se ejecuta usando el valor actualizado self.VAR == 8y el recién definido test_data el método se ejecuta contra el atributo heredado self.DATA == 4. Tal herencia de métodos y atributos brinda una forma flexible de reutilizar o modificar la funcionalidad compartida entre diferentes grupos de casos de prueba.

  • Es cierto que soy un novato en pytest, pero no pude fixt así como definirse fuera del TestGroup clase, y luego simplemente solicitó donde sea necesario?

    – Jon T.

    6 de marzo de 2021 a las 10:26

  • @JonT Ciertamente podría. Hasta donde yo sé, la única diferencia práctica entre definir fixt dentro vs fuera de TestGroup se reduce a cuestiones de alcance.

    – Jasha

    6 de marzo de 2021 a las 22:42


avatar de usuario
ladrón de mentes

Por lo general, en las pruebas unitarias, el objeto de nuestras pruebas es una sola función. Es decir, una sola función da lugar a múltiples pruebas. Al leer el código de prueba, es útil agrupar las pruebas para una sola unidad de alguna manera (lo que también nos permite, por ejemplo, ejecutar todas las pruebas para una función específica), por lo que esto nos deja con dos opciones:

  1. Coloque todas las pruebas para cada función en un módulo dedicado
  2. Poner todas las pruebas para cada función en una clase

En el primer enfoque, todavía estaríamos interesados ​​en agrupar todas las pruebas relacionadas con un módulo fuente (por ejemplo, utils.py) de alguna manera. Ahora, dado que ya estamos usando módulos para agrupar pruebas para un funciónesto significa que nos gustaría usar un paquete para agrupar pruebas para un módulo de origen.

El resultado es una fuente. función mapas a una prueba móduloy una fuente módulo mapas a una prueba paquete.

En el segundo enfoque, en su lugar, tendríamos un mapa de función fuente para una clase de prueba (por ejemplo, my_function() -> TestMyFunction) y un módulo de origen se asigna a un módulo de prueba (p. ej. utils.py -> test_utils.py).

Depende de la situación, tal vez, pero el segundo enfoque, es decir, una clase de pruebas para cada función que está probando, me parece más claro. Además, si estamos probando la fuente clases/métodos, entonces podríamos simplemente usar una jerarquía heredada de clases de prueba, y aún así conservar el módulo de origen -> una asignación de módulo de prueba.

Finalmente, otro beneficio de cualquier enfoque sobre un archivo plano que contiene pruebas para múltiples funciones es que con clases/módulos que ya identifican qué función se está probando, puede tener mejores nombres para las pruebas reales, por ejemplo test_does_x y test_handles_y en vez de test_my_function_does_x y test_my_function_handles_y.

En JavaScript jasmine las pruebas están estructuradas con describir y eso métodos: ¿Cuál es la diferencia entre describe y it en Jest?

Aquí hay una implementación de Python de ese concepto:

https://pypi.org/project/pytest-describe/

Una desventaja podría ser el retraso en el soporte para IDE. Supongo que para encontrar los métodos de prueba, su nombre debe comenzar con “test_” en lugar de “it_”.

def describe_list():

    @pytest.fixture
    def list():
        return []

    def describe_append():

        def adds_to_end_of_list(list):
            list.append('foo')
            list.append('bar')
            assert list == ['foo', 'bar']

    def describe_remove():

        @pytest.fixture
        def list():
            return ['foo', 'bar']

        def removes_item_from_list(list):
            list.remove('foo')
            assert list == ['bar']

¿Ha sido útil esta solución?