pegamento
Función de prueba Necesito pasar parámetros y ver que la salida coincida con la salida esperada.
Es fácil cuando la respuesta de la función es solo una pequeña matriz o una cadena de una línea que se puede definir dentro de la función de prueba, pero supongamos que la función que pruebo modifica un archivo de configuración que puede ser enorme. O la matriz resultante tiene una longitud de 4 líneas si la defino explícitamente. ¿Dónde lo guardo para que mis pruebas permanezcan limpias y fáciles de mantener?
En este momento, si eso es una cadena, simplemente coloco un archivo cerca del .py
probar y hacer open()
dentro de la prueba:
def test_if_it_works():
with open('expected_asnwer_from_some_function.txt') as res_file:
expected_data = res_file.read()
input_data = ... # Maybe loaded from a file as well
assert expected_data == if_it_works(input_data)
Veo muchos problemas con este enfoque, como el problema de mantener este archivo actualizado. Se ve mal también. Puedo hacer las cosas probablemente mejor moviendo esto a un accesorio:
@pytest.fixture
def expected_data()
with open('expected_asnwer_from_some_function.txt') as res_file:
expected_data = res_file.read()
return expected_data
@pytest.fixture
def input_data()
return '1,2,3,4'
def test_if_it_works(input_data, expected_data):
assert expected_data == if_it_works(input_data)
Eso solo mueve el problema a otro lugar y, por lo general, necesito probar si la función funciona en caso de entrada vacía, entrada con un solo elemento o múltiples elementos, por lo que debería crear un gran dispositivo que incluya los tres casos o múltiples dispositivos. Al final, el código se vuelve bastante desordenado.
Si una función espera un diccionario complicado como entrada o devuelve el diccionario del mismo tamaño enorme, el código de prueba se vuelve feo:
@pytest.fixture
def input_data():
# It's just an example
return {['one_value': 3, 'one_value': 3, 'one_value': 3,
'anotherky': 3, 'somedata': 'somestring'],
['login': 3, 'ip_address': 32, 'value': 53,
'one_value': 3], ['one_vae': 3, 'password': 13, 'lue': 3]}
Es bastante difícil leer las pruebas con estos accesorios y mantenerlos actualizados.
Actualizar
Después de buscar un rato, encontré una biblioteca que resolvió una parte de un problema cuando, en lugar de grandes archivos de configuración, tenía grandes respuestas HTML. Es betamax.
Para un uso más fácil, creé un accesorio:
from betamax import Betamax
@pytest.fixture
def session(request):
session = requests.Session()
recorder = Betamax(session)
recorder.use_cassette(os.path.join(os.path.dirname(__file__), 'fixtures', request.function.__name__)
recorder.start()
request.addfinalizer(recorder.stop)
return session
Así que ahora en mis pruebas solo uso el session
accesorio y cada solicitud que hago se serializa automáticamente en el fixtures/test_name.json
para que la próxima vez que ejecute la prueba, en lugar de hacer una biblioteca de solicitud HTTP real, la cargue desde el sistema de archivos:
def test_if_response_is_ok(session):
r = session.get("http://google.com")
Es bastante útil porque para mantener estos accesorios actualizados solo necesito limpiar el fixtures
carpeta y vuelva a ejecutar mis pruebas.
fabio menegazzo
Tuve un problema similar una vez, donde tengo que probar el archivo de configuración contra un archivo esperado. Así lo arreglé:
-
Cree una carpeta con el mismo nombre de su módulo de prueba y en la misma ubicación. Ponga todos sus archivos esperados dentro de esa carpeta.
test_foo/ expected_config_1.ini expected_config_2.ini test_foo.py
-
Cree un accesorio responsable de mover el contenido de esta carpeta a un archivo temporal. hice uso de
tmpdir
accesorio para este asunto.from __future__ import unicode_literals from distutils import dir_util from pytest import fixture import os @fixture def datadir(tmpdir, request): ''' Fixture responsible for searching a folder with the same name of test module and, if available, moving all contents to a temporary directory so tests can use them freely. ''' filename = request.module.__file__ test_dir, _ = os.path.splitext(filename) if os.path.isdir(test_dir): dir_util.copy_tree(test_dir, bytes(tmpdir)) return tmpdir
Importante: Si está utilizando Python 3, reemplace
dir_util.copy_tree(test_dir, bytes(tmpdir))
condir_util.copy_tree(test_dir, str(tmpdir))
. -
Usa tu nuevo accesorio.
def test_foo(datadir): expected_config_1 = datadir.join('expected_config_1.ini') expected_config_2 = datadir.join('expected_config_2.ini')
Recordar: datadir
es lo mismo que tmpdir
accesorio, además de la capacidad de trabajar con los archivos esperados colocados en una carpeta con el mismo nombre de módulo de prueba.
-
Esto tiene el beneficio adicional de que las pruebas no pueden interferir entre sí, ya que cada prueba tendrá una copia de los archivos de datos.
–Bruno Oliveira
14/04/2015 a las 15:49
-
Así es. De esta forma, puede ejecutar pruebas con varios núcleos sin preocuparse por la concurrencia de recursos (en este caso, los archivos esperados).
– Fabio Menegazzo
14/04/2015 a las 15:55
-
Sí, probablemente lo haré, pero pensé que tal vez sería demasiado torpe. Por cierto, no entendí lo de la concurrencia de recursos. Los archivos son de solo lectura, ¿qué podría salir mal?
– Pegamento
15 de abril de 2015 a las 10:03
-
Tienes razón. Para los archivos de solo lectura, todo debería funcionar bien, a menos que dichos archivos se bloqueen por algún motivo. Los problemas de simultaneidad ocurrirán con frecuencia al crear o cambiar archivos utilizados por más de una prueba.
– Fabio Menegazzo
16 de abril de 2015 a las 11:27
-
en pitón 3
dir_util.copy_tree(test_dir, str(tmpdir))
se requiere para hacer este trabajo.– ARF
15 de enero de 2017 a las 11:42
Yo creo Pytest-archivos de datos puede ser de gran ayuda. Desafortunadamente, parece que ya no se mantiene mucho. Por el momento, está funcionando bien.
Aquí hay un ejemplo simple tomado de los documentos:
import os
import pytest
@pytest.mark.datafiles('/opt/big_files/film1.mp4')
def test_fast_forward(datafiles):
path = str(datafiles) # Convert from py.path object to path (str)
assert len(os.listdir(path)) == 1
assert os.path.isfile(os.path.join(path, 'film1.mp4'))
#assert some_operation(os.path.join(path, 'film1.mp4')) == expected_result
# Using py.path syntax
assert len(datafiles.listdir()) == 1
assert (datafiles / 'film1.mp4').check(file=1)
-
Desde la entrada de PyPi vinculada: Nota sobre el mantenimiento: este proyecto se mantiene y se abordarán los informes de errores o las solicitudes de extracción. Hay poca actividad porque simplemente funciona y no se requieren cambios.
– iled
18 mayo 2020 a las 19:53
-
-
Como beneficio de pytest-datafiles, ¿por qué uno se preocuparía de que los archivos de datos se modifiquen si solo lee y no escribe?
– John
19 de agosto de 2022 a las 5:38
-
@rdmolony ¡Gracias!
pytest-datadir
parece ajustarse mejor a mis necesidades, ya que puede calcular la ruta del directorio de datos adyacente en lugar de que yo tenga que inventarlo. La forma de hacerlo es solicitar un accesorio llamadodatadir
(cual espathlib.Path
objeto) en mis argumentos de prueba, y ¡listo!– vintprox
26 de marzo a las 20:54
Si solo tiene algunas pruebas, ¿por qué no incluir los datos como una cadena literal?
expected_data = """
Your data here...
"""
Si tiene un puñado, o los datos esperados son realmente largos, creo que su uso de accesorios tiene sentido.
Sin embargo, si tiene muchos, quizás una solución diferente sería mejor. De hecho, para un proyecto tengo más de cien archivos de entrada y salida esperada. Así que construí mi propio marco de prueba (más o menos). Usé Nose, pero PyTest también funcionaría. Creé un generador de prueba que recorrió el directorio de archivos de prueba. Para cada archivo de entrada, se realizó una prueba que comparaba la salida real con la salida esperada (PyTest lo llama parametrización). Luego documenté mi marco para que otros pudieran usarlo. Para revisar y/o editar las pruebas, solo edita los archivos de entrada y/o salida esperados y nunca necesita mirar el archivo de prueba de python. Para permitir que diferentes archivos de entrada tengan diferentes opciones definidas, también creé un archivo de configuración YAML para cada directorio (JSON también funcionaría para mantener bajas las dependencias). Los datos YAML consisten en un diccionario donde cada clave es el nombre del archivo de entrada y el valor es un diccionario de palabras clave que pasarán a la función que se está probando junto con el archivo de entrada. Si estás interesado, aquí está el código fuente y documentación. Recientemente jugué con la idea de definir las opciones como Unittests aquí (requiere solo la lib unittest incorporada) pero no estoy seguro de si me gusta.
-
Bueno, cuando uso literales de cadena, casi siempre tengo problemas con el formato correcto. Si algún comando de un
subprocess
devuelve un resultado que quiero usar, contendrá símbolos especiales que desaparecerán cuando solo lo copie y pegue como cadena literal. Echaré un vistazo a tu código. Gracias. Desafortunadamente, eso parece una reinvención de la rueda. Como si nadie hubiera tenido el mismo problema antes.– Pegamento
14 de abril de 2015 a las 14:16
-
¿A qué tipo de caracteres especiales te refieres? ¿No existirían esas diferencias también con un archivo externo? No estoy seguro de cómo eso es un factor.
– Waylán
14 de abril de 2015 a las 15:07
-
Por lo general, es una mezcla de tabulaciones en lugar de espacios. Entonces, cuando copie la salida de la herramienta de consola en el literal de cadena y luego compare, difieren. Traté de agregar
\t
a la cadena que va y viene y finalmente serializa la cadena de salida. Esto también podría ser malo si los datos son de tipo binario o tienen una codificación extraña.– Pegamento
14/04/2015 a las 18:26
Piense si todo el contenido del archivo de configuración realmente necesita ser probado.
Si solo se deben verificar varios valores o subcadenas, prepare una plantilla esperada para esa configuración. Los lugares probados se marcarán como “variables” con alguna sintaxis especial. Luego prepare una lista esperada separada de los valores para las variables en la plantilla. Esta lista esperada se puede almacenar como un archivo separado o directamente en el código fuente.
Ejemplo para la plantilla:
ALLOWED_HOSTS = ['{host}']
DEBUG = {debug}
DEFAULT_FROM_EMAIL = '{email}'
Aquí, las variables de la plantilla se colocan entre llaves.
Los valores esperados pueden verse como:
host = www.example.com
debug = False
email = webmaster@example.com
o incluso como una simple lista separada por comas:
www.example.com, False, webmaster@example.com
Luego, su código de prueba puede producir el archivo esperado a partir de la plantilla reemplazando las variables con los valores esperados. Y el archivo esperado se compara con el real.
Mantener la plantilla y los valores esperados por separado tiene la ventaja de que puede tener muchos conjuntos de datos de prueba usando la misma plantilla.
Probando solo variables
Un enfoque aún mejor es que el método de generación de configuración produce solo los valores necesarios para el archivo de configuración. Estos valores se pueden insertar fácilmente en la plantilla mediante otro método. Pero la ventaja es que el código de prueba puede comparar directamente todas las variables de configuración por separado y de manera clara.
Plantillas
Si bien es fácil reemplazar las variables con los valores necesarios en la plantilla, existen bibliotecas de plantillas listas, que permiten hacerlo solo en una línea. Estos son solo algunos ejemplos: Django, Jinja, mako
Mírelo desde el punto de vista del desarrollador, que tiene una falla de TC y necesita depurarlo. Tiene un nombre de caso de prueba, por lo que va a la función de este nombre. Si mantiene los datos esperados por separado, lo obliga a saltar de un lado a otro para compararlos. Siempre que pueda mantener la entrada de prueba y la entrada esperada dentro de un caso de prueba o lo más cerca posible, en mi humilde opinión.
– m.wasowski
14/04/2015 a las 12:37
@m.wasowski entonces, ¿cuál es su sugerencia? ¿Para mantener grandes cantidades de datos dentro del cuerpo de una función de prueba? ¿Y si es uno binario por ejemplo?..
– Pegamento
14 de abril de 2015 a las 14:17
binary no se lee bien, de todos modos, por lo que es una historia diferente. Solo digo que, en las pruebas, mantener los datos cerca del uso es al menos tan importante como SECO. Todo depende del caso. Por supuesto, le sugiero que sangre sus datos… esto
return
que escribiste es realmente feo y difícil de entender.– m.wasowski
14/04/2015 a las 15:30