Clases y métodos de prueba
Además de escribir funciones de prueba, Pytest permite usar clases. Como ya hemos mencionado, no hay necesidad de herencia y las clases de prueba siguen algunas reglas sencillas. El uso de clases proporciona más flexibilidad y reutilización. Como verá a continuación, Pytest se mantiene fuera del camino y evita forzarle a escribir pruebas de una manera determinada.
Al igual que las funciones, todavía puede escribir aserciones mediante la instrucción assert.
Compilación de una clase de prueba
Vamos a usar un escenario real para ver cómo pueden ayudar las clases de prueba. La siguiente función comprueba si un archivo determinado contiene "sí" en su contenido. Si es así, devuelve True. Si el archivo no existe o si contiene "no" en su contenido, devuelve False. Este escenario es común en las tareas asincrónicas que usan el sistema de archivos para indicar la finalización.
Este es el aspecto de la función:
import os
def is_done(path):
if not os.path.exists(path):
return False
with open(path) as _f:
contents = _f.read()
if "yes" in contents.lower():
return True
elif "no" in contents.lower():
return False
Ahora, este es el aspecto de una clase con dos pruebas (una para cada condición) en un archivo denominado test_files.py:
class TestIsDone:
def test_yes(self):
with open("/tmp/test_file", "w") as _f:
_f.write("yes")
assert is_done("/tmp/test_file") is True
def test_no(self):
with open("/tmp/test_file", "w") as _f:
_f.write("no")
assert is_done("/tmp/test_file") is False
Precaución
Los métodos de prueba usan la ruta /tmp para un archivo de prueba temporal porque es más fácil de usar en este ejemplo. Sin embargo, si necesita usar archivos temporales, considere la posibilidad de usar una biblioteca como tempfile que pueda crearlos (y quitarlos) de forma segura. No todos los sistemas tienen un directorio /tmp y es posible que esa ubicación no sea temporal en función del sistema operativo.
La ejecución de las pruebas con la marca -v para aumentar el nivel de detalle muestra las pruebas superadas:
pytest -v test_files.py
============================= 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_files.py::TestIsDone::test_yes PASSED [ 50%]
test_files.py::TestIsDone::test_no PASSED [100%]
============================== 2 passed in 0.00s ===============================
Aunque se superan las pruebas, parecen repetitivas y también dejan archivos una vez finalizada la prueba. Antes de ver cómo podemos mejorarlas, vamos a tratar los métodos auxiliares en la sección siguiente.
Métodos auxiliares
En una clase de prueba, hay algunos métodos que puede usar para configurar y anular la ejecución de pruebas. Pytest las ejecuta automáticamente si se definen. Para usar estos métodos, debe saber que tienen un orden y un comportamiento específicos.
-
setup: se ejecuta una vez antes de cada prueba de una clase. -
teardown: se ejecuta una vez después de cada prueba en una clase . -
setup_class: se ejecuta una vez antes de todas las pruebas de una clase. -
teardown_class: se ejecuta una vez después de todas las pruebas de una clase.
Cuando las pruebas requieren recursos similares (o idénticos) para funcionar, resulta útil escribir métodos de instalación. Lo ideal es que una prueba no deje recursos cuando se complete, por lo que los métodos de anulación pueden ayudar en la limpieza de pruebas en esas situaciones.
Limpieza
Vamos a comprobar una clase de prueba actualizada que limpia los archivos después de cada prueba:
class TestIsDone:
def teardown(self):
if os.path.exists("/tmp/test_file"):
os.remove("/tmp/test_file")
def test_yes(self):
with open("/tmp/test_file", "w") as _f:
_f.write("yes")
assert is_done("/tmp/test_file") is True
def test_no(self):
with open("/tmp/test_file", "w") as _f:
_f.write("no")
assert is_done("/tmp/test_file") is False
Dado que usamos el método teardown(), esta clase de prueba ya no deja atrás un /tmp/test_file.
Configurar
Otra mejora que podemos realizar en esta clase es agregar una variable que apunte al archivo. Puesto que el archivo se declara ahora en seis lugares, cualquier cambio en la ruta de acceso significaría cambiarlo en todos esos puntos. En este ejemplo se muestra cómo se ve la clase con un método setup() agregado que declara la variable path:
class TestIsDone:
def setup(self):
self.tmp_file = "/tmp/test_file"
def teardown(self):
if os.path.exists(self.tmp_file):
os.remove(self.tmp_file)
def test_yes(self):
with open(self.tmp_file, "w") as _f:
_f.write("yes")
assert is_done(self.tmp_file) is True
def test_no(self):
with open(self.tmp_file, "w") as _f:
_f.write("no")
assert is_done(self.tmp_file) is False
Métodos del asistente personalizados
Puede crear métodos del asistente personalizados en una clase. Estos métodos no deben tener el prefijo con el nombre test y no se pueden denominar como los métodos de configuración o limpieza. En la TestIsDone clase , podríamos automatizar la creación del archivo temporal en un asistente personalizado. Ese método auxiliar personalizado podría tener un aspecto similar al de este ejemplo:
def write_tmp_file(self, content):
with open(self.tmp_file, "w") as _f:
_f.write(content)
Pytest no ejecuta automáticamente el método write_tmp_file() y otros métodos pueden llamarlo directamente para ahorrarse tareas repetitivas como escribir en un archivo.
La clase completa es similar a este ejemplo, después de actualizar los métodos de prueba para usar el asistente personalizado:
class TestIsDone:
def setup(self):
self.tmp_file = "/tmp/test_file"
def teardown(self):
if os.path.exists(self.tmp_file):
os.remove(self.tmp_file)
def write_tmp_file(self, content):
with open(self.tmp_file, "w") as _f:
_f.write(content)
def test_yes(self):
self.write_tmp_file("yes")
assert is_done(self.tmp_file) is True
def test_no(self):
self.write_tmp_file("no")
assert is_done(self.tmp_file) is False
Cuándo usar una clase en lugar de una función
No hay reglas estrictas en cuanto a cuándo usar una clase en lugar de una función. Siempre es una buena idea seguir las convenciones de los proyectos y equipos actuales con los que trabaja. Estas son algunas preguntas generales que puede ayudarle a determinar cuándo usar una clase:
- ¿Las pruebas necesitan código auxiliar de configuración o limpieza similares?
- ¿La agrupación de las pruebas tiene sentido lógico?
- ¿Hay al menos algunas pruebas en el conjunto de pruebas?
- ¿Las pruebas podrían beneficiarse de un conjunto común de funciones auxiliares?