pytest: pruebas reutilizables para diferentes implementaciones de la misma interfaz

Imagina que he implementado una utilidad (tal vez una clase) llamada Bar en un módulo foo , y he escrito las siguientes pruebas para ello.

test_foo.py:

 from foo import Bar as Implementation from pytest import mark @mark.parametrize(, ) def test_one():  @mark.parametrize(, ) def test_two():   

Ahora imagine que, en el futuro, espero que se escriban diferentes implementaciones de la misma interfaz. Me gustaría que esas implementaciones puedan reutilizar las pruebas escritas para el conjunto de pruebas anterior: las únicas cosas que deben cambiar son

  1. La importación de la Implementation
  2. , etc.

Así que estoy buscando una manera de escribir las pruebas anteriores de forma reutilizable, que permitiría a los autores de nuevas implementaciones de la interfaz poder usar las pruebas inyectando la implementación y los datos de prueba en ellas, sin tener que modificar la Archivo que contiene la especificación original de las pruebas.

¿Cuál sería una buena forma idiomática de hacer esto en pytest?

================================================== ==================

================================================== ==================

Aquí hay una versión de unittest que (no es bonita pero) funciona.

define_tests.py:

 # Single, reusable definition of tests for the interface. Authors of # new implementations of the interface merely have to provide the test # data, as class attributes of a class which inherits # unittest.TestCase AND this class. class TheTests(): def test_foo(self): # Faking pytest.mark.parametrize by looping for args, in_, out in self.test_foo_data: self.assertEqual(self.Implementation(*args).foo(in_), out) def test_bar(self): # Faking pytest.mark.parametrize by looping for args, in_, out in self.test_bar_data: self.assertEqual(self.Implementation(*args).bar(in_), out) 

v1.py:

 # One implementation of the interface class Implementation: def __init__(self, a,b): self.n = a+b def foo(self, n): return self.n + n def bar(self, n): return self.n - n 

v1_test.py:

 # Test for one implementation of the interface from v1 import Implementation from define_tests import TheTests from unittest import TestCase # Hook into testing framework by inheriting unittest.TestCase and reuse # the tests which *each and every* implementation of the interface must # pass, by inheritance from define_tests.TheTests class FooTests(TestCase, TheTests): Implementation = Implementation test_foo_data = (((1,2), 3, 6), ((4,5), 6, 15)) test_bar_data = (((1,2), 3, 0), ((4,5), 6, 3)) 

Cualquiera (incluso un cliente de la biblioteca) escribe otra implementación de esta interfaz

  • puede reutilizar el conjunto de pruebas definido en define_tests.py
  • inyectar datos de prueba propios en las pruebas
  • Sin modificar ninguno de los archivos originales.

Este es un gran caso de uso para dispositivos de prueba parametrizados .

Tu código podría verse algo como esto:

 from foo import Bar, Baz @pytest.fixture(params=[Bar, Baz]) def Implementation(request): return request.param def test_one(Implementation): assert Implementation().frobnicate() 

Esto tendría que ejecutar test_one dos veces: una vez donde Implementación = Barra y una vez donde Implementación = Baz.

Tenga en cuenta que, dado que la implementación es solo un elemento, puede cambiar su scope o hacer más configuraciones (tal vez ejemplificar la clase, tal vez configurarla de alguna manera).

Si se usa con el decorador pytest.mark.parametrize , pytest generará todas las permutaciones. Por ejemplo, asumiendo el código anterior y este código aquí:

 @pytest.mark.parametrize('thing', [1, 2]) def test_two(Implementation, thing): assert Implementation(thing).foo == thing 

test_two se ejecutará cuatro veces, con las siguientes configuraciones:

  • Implementación = Bar, cosa = 1
  • Implementación = Bar, cosa = 2
  • Implementación = Baz, cosa = 1
  • Implementación = Baz, cosa = 2

No puede hacerlo sin herencia de clase, pero no tiene que usar unittest.TestCase. Para hacerlo más atractivo puedes usar accesorios.

Le permite, por ejemplo, parametrizar aparatos, o usar otros arreglos.

Intento crear un ejemplo simple.

 class SomeTest: @pytest.fixture def implementation(self): return "A" def test_a(self, implementation): assert "A" == implementation class OtherTest(SomeTest): @pytest.fixture(params=["B", "C"]) def implementation(self, request): return request.param def test_a(self, implementation): """ the "implementation" fixture is not accessible out of class """ assert "A" == implementation 

y la segunda prueba falla

  def test_a(self, implementation): > assert "A" == implementation E assert 'A' == 'B' E - A E + B def test_a(self, implementation): > assert "A" == implementation E assert 'A' == 'C' E - A E + C def test_a(implementation): fixture 'implementation' not found 

No olvides que debes definir python_class = *Test en pytest.ini