Prueba de Python con pytest. Accesorios incorporados, Capítulo 4

Atrás Siguiente


Los accesorios incorporados que vienen con pytest pueden ayudarlo a hacer algunas cosas bastante útiles en sus pruebas de manera fácil y natural. Por ejemplo, además de manejar archivos temporales, pytest incluye accesorios incorporados para acceder a los parámetros de la línea de comandos, comunicación entre sesiones de prueba, verificación de flujos de salida, cambio de variables de entorno y alertas de sondeo.



El código fuente para el proyecto Tareas, así como para todas las pruebas que se muestran en este libro, está disponible en el enlace en la página web del libro en pragprog.com . No necesita descargar el código fuente para comprender el código de prueba; El código de prueba se presenta de forma conveniente en los ejemplos. Pero para seguir las tareas del proyecto, o adaptar ejemplos de prueba para verificar su propio proyecto (¡sus manos están desatadas!), Debe ir a la página web del libro para descargar el trabajo. Allí, en la página web del libro, hay un enlace a la publicación de erratas y al foro de discusión .

Debajo del spoiler hay una lista de artículos en esta serie.



En el capítulo anterior, examinó qué son los accesorios, cómo escribirlos y cómo usarlos para los datos de prueba, así como para el código de configuración y desmontaje.


También usó conftest.py para compartir accesorios entre pruebas en múltiples archivos de prueba. Al final del capítulo 3, se instalaron los siguientes accesorios en los accesorios de Pytest en la página 49 del proyecto Tareas: tasks_db_session , tasks_just_a_few , tasks_mult_per_owner , tasks_db , db_with_3_tasks y db_with_multi_per_owner definido en conftest.py función que puede ser utilizada por cualquier función testtest que puede ser utilizada por cualquier función testtest que pueda ser utilizada por cualquier función de prueba los necesita


La reutilización de accesorios regulares es una idea tan buena que los desarrolladores de pytest han incluido algunos accesorios comúnmente requeridos en pytest. Ya ha visto cómo tmpdir y tmpdir_factory son utilizados por el proyecto Tareas en la sección de cambio de alcance para los accesorios del proyecto Tareas en la página 59. Los discutirá con más detalle en este capítulo.


Los accesorios incorporados que vienen con pytest pueden ayudarlo a hacer algunas cosas bastante útiles en sus pruebas de manera fácil y natural. Por ejemplo, además de manejar archivos temporales, pytest incluye accesorios incorporados para acceder a los parámetros de la línea de comandos, comunicación entre sesiones de prueba, verificación de flujos de salida, cambio de variables de entorno y alertas de sondeo. Los accesorios incorporados son extensiones de la funcionalidad central de pytest. Veamos ahora algunos de los accesorios en línea más utilizados en orden.


Usando tmpdir y tmpdir_factory


Si está probando algo que lee, escribe o modifica archivos, puede usar tmpdir para crear archivos o directorios utilizados por una sola prueba, y puede usar tmpdir_factory cuando desee configurar un directorio para varias pruebas.


El tmpdir tmpdir tiene un alcance de función y el dispositivo tmpdir_factory tiene un alcance de sesión. Cualquier prueba individual que requiera un directorio o archivo temporal para una sola prueba puede usar tmpdir . Esto también es cierto para los dispositivos, que personalizan el directorio o archivo que debe recrearse para cada función de prueba.


Aquí hay un ejemplo simple usando tmpdir :


ch4/test_tmpdir.py

 def test_tmpdir(tmpdir): # tmpdir    ,    # join()  ,    , #     a_file = tmpdir.join('something.txt') #    a_sub_dir = tmpdir.mkdir('anything') #      (  ) another_file = a_sub_dir.join('something_else.txt') #    'something.txt' a_file.write('contents may settle during shipping') #    'anything/something_else.txt' another_file.write('something different') #      assert a_file.read() == 'contents may settle during shipping' assert another_file.read() == 'something different' 

El valor devuelto por tmpdir es un objeto de tipo py.path.local.1 que parece ser todo lo que necesitamos para directorios y archivos temporales. Sin embargo, hay un truco. Dado que el dispositivo tmpdir define como un alcance de función , tmpdir no se puede utilizar para crear carpetas o archivos que deben estar disponibles durante más de una función de prueba. Para dispositivos con un alcance que no sea una función (clase, módulo, sesión), tmpdir_factory está disponible.


El tmpdir_factory tmpdir_factory tmpdir_factory muy similar a tmpdir , pero tiene una interfaz diferente. Como se describe en la sección “Especificación del dispositivo de alcance”, en la página 56, los dispositivos de área de función se ejecutan una vez para cada función de prueba, los dispositivos de región de módulo se ejecutan una vez por módulo, los dispositivos de clase una vez para cada clase y las pruebas de validación de área trabajar una vez por sesión. Por lo tanto, los recursos creados en los registros del área de sesión tienen una vida útil de toda la sesión. Para mostrar cuán similares son tmpdir y tmpdir_factory , tmpdir ejemplo de tmpdir , donde tmpdir_factory :


ch4 / test_tmpdir.py

 def test_tmpdir_factory(tmpdir_factory): #      . a_dir   # ,    tmpdir a_dir = tmpdir_factory.mktemp('mydir') # base_temp    'mydir'    #  getbasetemp(),  # ,    base_temp = tmpdir_factory.getbasetemp() print('base:', base_temp) #       , #    ' test_tmpdir ()',   , #    a_dir  tmpdir a_file = a_dir.join('something.txt') a_sub_dir = a_dir.mkdir('anything') another_file = a_sub_dir.join('something_else.txt') a_file.write('contents may settle during shipping') another_file.write('something different') assert a_file.read() == 'contents may settle during shipping' assert another_file.read() == 'something different' 

La primera línea usa mktemp('mydir') para crear el directorio y lo almacena en a_dir . Para el resto de la función, puede usar a_dir de la misma manera que tmpdir regresó del tmpdir .


En la segunda línea del ejemplo tmpdir_factory , la función getbasetemp() devuelve el directorio base utilizado para esta sesión. La declaración de impresión en el ejemplo es necesaria para que pueda ver el directorio en su sistema. Veamos dónde está:


 $ cd /path/to/code/ch4 $ pytest -q -s test_tmpdir.py::test_tmpdir_factory base: /private/var/folders/53/zv4j_zc506x2xq25l31qxvxm0000gn/T/pytest-of-okken/pytest-732 . 1 passed in 0.04 seconds 

Este directorio base depende del sistema y del usuario, y pytest - NUM cambia para cada sesión con NUM creciente. El directorio base se deja solo después de la sesión. pytest lo limpia, y solo quedan los pocos directorios base temporales más recientes en el sistema, lo cual está bien si está impaciente por revisar los archivos después de una ejecución de prueba.


También puede especificar su propio directorio base si lo necesita con pytest --basetemp=mydir .


Uso de directorios temporales para otras áreas.


Obtenemos directorios temporales y archivos de área de sesión del tmpdir_factory , y directorios de funciones y archivos de región del tmpdir . ¿Pero qué hay de otras áreas? ¿Qué sucede si necesitamos un directorio temporal del alcance de un módulo o clase? Para hacer esto, creamos otro dispositivo de la región del tamaño deseado y para esto debemos usar tmpdir_factory .


Por ejemplo, supongamos que tenemos un módulo lleno de pruebas, y muchas de ellas deberían poder leer algunos datos de un archivo json . Pudimos colocar el dispositivo de volumen del dispositivo en el módulo mismo o en el archivo conftest.py , que configura el archivo de datos de la siguiente manera:


ch4 / autores / conftest.py

 """Demonstrate tmpdir_factory.""" import json import pytest @pytest.fixture(scope='module') def author_file_json(tmpdir_factory): """     .""" python_author_data = { 'Ned': {'City': 'Boston'}, 'Brian': {'City': 'Portland'}, 'Luciano': {'City': 'Sau Paulo'} } file = tmpdir_factory.mktemp('data').join('author_file.json') print('file:{}'.format(str(file))) with file.open('w') as f: json.dump(python_author_data, f) return file 

El author_file_json() crea un directorio temporal llamado data y crea un archivo llamado author_file.json en el directorio de datos. Luego escribe el diccionario python_author_data como json . Como se trata de un módulo de área de fijación, se creará un archivo json solo una vez para cada módulo utilizando la prueba:


ch4 / autores / test_authors.py

 """ ,    .""" import json def test_brian_in_portland(author_file_json): """,   .""" with author_file_json.open() as f: authors = json.load(f) assert authors['Brian']['City'] == 'Portland' def test_all_have_cities(author_file_json): """        .""" with author_file_json.open() as f: authors = json.load(f) for a in authors: assert len(authors[a]['City']) > 0 

Ambas pruebas utilizarán el mismo archivo JSON. Si un archivo de datos de prueba funciona para varias pruebas, no tiene sentido volver a crearlo para ambas pruebas.


Usando pytestconfig


Usando el accesorio incorporado pytestconfig, puede controlar cómo funciona pytest con argumentos y opciones de línea de comandos, archivos de configuración, complementos y el directorio desde el que inició pytest. El dispositivo pytestconfig es un acceso directo a request.config, y en la documentación de pytest a veces se lo denomina " el objeto de configuración pytest " (objeto de configuración pytest).


Para saber cómo funciona pytestconfig , puede ver cómo agregar un parámetro de línea de comando personalizado y leer el valor del parámetro de la prueba. Puede leer el valor de los parámetros de la línea de comandos directamente desde pytestconfig, pero para agregar un parámetro y analizarlo, debe agregar una función de enlace. Las funciones de enlace, que describo con más detalle en el Capítulo 5, "Complementos", en la página 95, son otra forma de controlar el comportamiento de pytest y a menudo se utilizan en complementos. Sin embargo, agregar una opción de línea de comando personalizada y leerlo desde pytestconfig está bastante extendido, por lo que quiero cubrir esto aquí.


Utilizaremos el enlace pytest pytest_addoption para agregar varios parámetros a los parámetros ya disponibles en la línea de comando pytest:


ch4 / pytestconfig / conftest.py

 def pytest_addoption(parser): parser.addoption("--myopt", action="store_true", help="some boolean option") parser.addoption("--foo", action="store", default="bar", help="foo: bar or baz") 

La adición de parámetros de línea de comando a través de pytest_addoption debe hacerse a través de complementos o en el archivo conftest.py ubicado en la parte superior de la estructura del directorio del proyecto. No debe hacer esto en el subdirectorio de prueba.


Las --myopt y --foo <value> se agregaron al código anterior, y la línea de ayuda se cambió como se muestra a continuación:


 $ cd /path/to/code/ch4/pytestconfig $ pytest --help usage: pytest [options] [file_or_dir] [file_or_dir] [...] ... custom options: --myopt some boolean option --foo=FOO foo: bar or baz ... 

Ahora podemos acceder a estas opciones desde la prueba:


ch4 / pytestconfig / test_config.py

 import pytest def test_option(pytestconfig): print('"foo" set to:', pytestconfig.getoption('foo')) print('"myopt" set to:', pytestconfig.getoption('myopt')) 

Veamos cómo funciona:


 $ pytest -s -q test_config.py::test_option "foo" set to: bar "myopt" set to: False .1 passed in 0.01 seconds $ pytest -s -q --myopt test_config.py::test_option "foo" set to: bar "myopt" set to: True .1 passed in 0.01 seconds $ pytest -s -q --myopt --foo baz test_config.py::test_option "foo" set to: baz "myopt" set to: True .1 passed in 0.01 seconds 

Dado que pytestconfig es un accesorio, también se puede obtener de otros accesorios. Puede hacer accesorios para nombres de opciones si lo desea, por ejemplo:


ch4 / pytestconfig / test_config.py

 @pytest.fixture() def foo(pytestconfig): return pytestconfig.option.foo @pytest.fixture() def myopt(pytestconfig): return pytestconfig.option.myopt def test_fixtures_for_options(foo, myopt): print('"foo" set to:', foo) print('"myopt" set to:', myopt) 

También puede acceder a los parámetros integrados, no solo a los agregados, así como a información sobre cómo se lanzó pytest (directorio, argumentos, etc.).


Aquí hay un ejemplo de varios valores y opciones de configuración:


 def test_pytestconfig(pytestconfig): print('args :', pytestconfig.args) print('inifile :', pytestconfig.inifile) print('invocation_dir :', pytestconfig.invocation_dir) print('rootdir :', pytestconfig.rootdir) print('-k EXPRESSION :', pytestconfig.getoption('keyword')) print('-v, --verbose :', pytestconfig.getoption('verbose')) print('-q, --quiet :', pytestconfig.getoption('quiet')) print('-l, --showlocals:', pytestconfig.getoption('showlocals')) print('--tb=style :', pytestconfig.getoption('tbstyle')) 

Volveremos a pytestconfig cuando demuestre los archivos ini en el capítulo 6, "Configuración", en la página 113.


Usando caché


Por lo general, los evaluadores creemos que cada prueba es lo más independiente posible de las otras pruebas. Debe asegurarse de que las dependencias de la contabilidad de pedidos no se hayan infiltrado. Me gustaría poder ejecutar o reiniciar cualquier prueba en cualquier orden y obtener el mismo resultado. Además, las sesiones de prueba deben ser repetibles y no cambiar el comportamiento según las sesiones de prueba anteriores.


Sin embargo, a veces transferir información de una sesión de prueba a otra puede ser muy útil. Cuando queremos pasar información a futuras sesiones de prueba, podemos hacerlo con el dispositivo de cache incorporado.


Fixture cache diseñado para almacenar información sobre una sesión de prueba y obtenerla en la siguiente. Un gran ejemplo de uso de permisos de cache para el beneficio del caso es la funcionalidad --last-failed --failed-first y --failed-first . Veamos cómo se almacenan los datos de estos indicadores en la memoria caché.


Aquí está el texto de ayuda para las --last-failed --failed-first y --failed-first , así como algunas opciones de cache :


 $ pytest --help ... --lf, --last-failed rerun only the tests that failed at the last run (or all if none failed) --ff, --failed-first run all tests but run the last failures first. This may re-order tests and thus lead to repeated fixture setup/teardown --cache-show show cache contents, don t perform collection or tests --cache-clear remove all cache contents at start of test run. ... 

Para verlos en acción, utilizaremos estas dos pruebas:


ch4 / cache / test_pass_fail.py


 def test_this_passes(): assert 1 == 1 def test_this_fails(): assert 1 == 2 

--tb=no usando --verbose para ver los nombres de las funciones, y --tb=no para ocultar el seguimiento de la pila:


 $ cd /path/to/code/ch4/cache $ pytest --verbose --tb=no test_pass_fail.py ==================== test session starts ==================== collected 2 items test_pass_fail.py::test_this_passes PASSED test_pass_fail.py::test_this_fails FAILED ============ 1 failed, 1 passed in 0.05 seconds ============= 

Si los vuelve a ejecutar con el --ff o --failed-first , las pruebas que fallaron anteriormente se ejecutarán primero y luego toda la sesión:


 $ pytest --verbose --tb=no --ff test_pass_fail.py ==================== test session starts ==================== run-last-failure: rerun last 1 failures first collected 2 items test_pass_fail.py::test_this_fails FAILED test_pass_fail.py::test_this_passes PASSED ============ 1 failed, 1 passed in 0.04 seconds ============= 

O puede usar --lf o --last-failed para ejecutar pruebas que fallaron la última vez:


 $ pytest --verbose --tb=no --lf test_pass_fail.py ==================== test session starts ==================== run-last-failure: rerun last 1 failures collected 2 items test_pass_fail.py::test_this_fails FAILED ==================== 1 tests deselected ===================== ========== 1 failed, 1 deselected in 0.05 seconds =========== 

Antes de ver cómo se almacenan los datos de bloqueo y cómo puede usar el mismo mecanismo, veamos otro ejemplo que hace que el valor de --lf y --ff aún más obvio.


Aquí hay una prueba parametrizada con una falla:


ch4 / cache / test_few_failures.py

 """Demonstrate -lf and -ff with failing tests.""" import pytest from pytest import approx testdata = [ # x, y, expected (1.01, 2.01, 3.02), (1e25, 1e23, 1.1e25), (1.23, 3.21, 4.44), (0.1, 0.2, 0.3), (1e25, 1e24, 1.1e25) ] @pytest.mark.parametrize("x,y,expected", testdata) def test_a(x, y, expected): """Demo approx().""" sum_ = x + y assert sum_ == approx(expected) 

Y a la salida:


 $ cd /path/to/code/ch4/cache $ pytest -q test_few_failures.py .F... ====================== FAILURES ====================== _________________________ test_a[1e+25-1e+23-1.1e+25] _________________________ x = 1e+25, y = 1e+23, expected = 1.1e+25 @pytest.mark.parametrize("x,y,expected", testdata) def test_a(x, y, expected): """Demo approx().""" sum_ = x + y > assert sum_ == approx(expected) E assert 1.01e+25 == 1.1e+25 ± 1.1e+19 E + where 1.1e+25 ± 1.1e+19 = approx(1.1e+25) test_few_failures.py:17: AssertionError 1 failed, 4 passed in 0.06 seconds 

Quizás pueda identificar el problema de inmediato. Pero imaginemos que la prueba es más larga y más complicada, y no es tan obvio lo que está mal aquí. Ejecutemos la prueba nuevamente para ver el error nuevamente. El caso de prueba se puede especificar en la línea de comando:


 $ pytest -q "test_few_failures.py::test_a[1e+25-1e+23-1.1e+25]" 

Si no desea copiar-pegar (copiar / pegar ) o hay varios casos desafortunados que desea reiniciar, entonces --lf mucho más fácil. Y si realmente está depurando una falla de prueba, otro indicador que puede aliviar la situación es --showlocals , o -l para abreviar:


 $ pytest -q --lf -l test_few_failures.py F ====================== FAILURES ====================== _________________________ test_a[1e+25-1e+23-1.1e+25] _________________________ x = 1e+25, y = 1e+23, expected = 1.1e+25 @pytest.mark.parametrize("x,y,expected", testdata) def test_a(x, y, expected): """Demo approx().""" sum_ = x + y > assert sum_ == approx(expected) E assert 1.01e+25 == 1.1e+25 ± 1.1e+19 E + where 1.1e+25 ± 1.1e+19 = approx(1.1e+25) expected = 1.1e+25 sum_ = 1.01e+25 x = 1e+25 y = 1e+23 test_few_failures.py:17: AssertionError ================= 4 tests deselected ================= 1 failed, 4 deselected in 0.05 seconds 

La razón del fracaso debería ser más obvia.


Para tener en cuenta que la prueba falló la última vez, hay un pequeño truco. pytest almacena información de error de prueba de la última sesión de prueba y puede ver la información guardada con --cache-show :


 $ pytest --cache-show ===================== test session starts ====================== ------------------------- cache values ------------------------- cache/lastfailed contains: {'test_few_failures.py::test_a[1e+25-1e+23-1.1e+25]': True} ================= no tests ran in 0.00 seconds ================= 

O puede buscar en el directorio de caché:


 $ cat .cache/v/cache/lastfailed { "test_few_failures.py::test_a[1e+25-1e+23-1.1e+25]": true } 

El interruptor --clear-cache permite borrar el caché antes de una sesión.


El caché se puede usar no solo para --lf y --ff . Escribamos un dispositivo que registre cuánto tiempo toman las pruebas, ahorra tiempo y la próxima vez que informa un error en las pruebas que toman el doble de tiempo que, por ejemplo, la última vez.


La interfaz para el dispositivo de caché es simple.


 cache.get(key, default) cache.set(key, value) 

Por convención, los nombres de clave comienzan con el nombre de su aplicación o complemento, seguidos de / , y continúan separando las secciones de nombre de clave con / . El valor que almacena puede ser cualquiera que se convierta a json , ya que está representado en el .cache directory .


Aquí está nuestro dispositivo utilizado para fijar el tiempo de prueba:


ch4 / cache / test_slower.py


 @pytest.fixture(autouse=True) def check_duration(request, cache): key = 'duration/' + request.node.nodeid.replace(':', '_') #   (nodeid)    #      .cache #    -     start_time = datetime.datetime.now() yield stop_time = datetime.datetime.now() this_duration = (stop_time - start_time).total_seconds() last_duration = cache.get(key, None) cache.set(key, this_duration) if last_duration is not None: errorstring = "       2-  " assert this_duration <= last_duration * 2, errorstring 

Como el dispositivo es de uso automático , no es necesario hacer referencia a él desde la prueba. El objeto de solicitud se usa para obtener el nodeid para usar en la clave. nodeid es un identificador único que funciona incluso con pruebas parametrizadas. Agregamos una clave con 'duración /' para ser residentes respetables del caché. El código anterior rendimiento se ejecuta antes de la función de prueba; el código después del rendimiento se ejecuta después de la función de prueba.


Ahora necesitamos algunas pruebas que tomen diferentes intervalos de tiempo:


ch4 / cache / test_slower.py


 @pytest.mark.parametrize('i', range(5)) def test_slow_stuff(i): time.sleep(random.random()) 

Dado que probablemente no desee escribir un montón de pruebas para esto, utilicé random y parametrización para generar fácilmente algunas pruebas que duermen durante un período de tiempo aleatorio, todo menos de un segundo. Veamos un par de veces cómo funciona esto:


 $ cd /path/to/code/ch4/cache $ pytest -q --cache-clear test_slower.py ..... 5 passed in 2.10 seconds $ pytest -q --tb=line test_slower.py ...E..E =================================== ERRORS ==================================== ___________________ ERROR at teardown of test_slow_stuff[1] ___________________ E AssertionError: test duration over 2x last duration assert 0.35702 <= (0.148009 * 2) ___________________ ERROR at teardown of test_slow_stuff[4] ___________________ E AssertionError: test duration over 2x last duration assert 0.888051 <= (0.324019 * 2) 5 passed, 2 error in 3.17 seconds 

Bueno, eso fue divertido. Veamos qué hay en el caché:


 $ pytest -q --cache-show -------------------------------- cache values --------------------------------- cache\lastfailed contains: {'test_slower.py::test_slow_stuff[2]': True, 'test_slower.py::test_slow_stuff[4]': True} cache\nodeids contains: ['test_slower.py::test_slow_stuff[0]', 'test_slower.py::test_slow_stuff[1]', 'test_slower.py::test_slow_stuff[2]', 'test_slower.py::test_slow_stuff[3]', 'test_slower.py::test_slow_stuff[4]'] cache\stepwise contains: [] duration\test_slower.py__test_slow_stuff[0] contains: 0.958055 duration\test_slower.py__test_slow_stuff[1] contains: 0.214012 duration\test_slower.py__test_slow_stuff[2] contains: 0.19001 duration\test_slower.py__test_slow_stuff[3] contains: 0.725041 duration\test_slower.py__test_slow_stuff[4] contains: 0.836048 no tests ran in 0.03 seconds 

Puede ver fácilmente los datos de duration separado de los datos de caché debido al prefijo de los nombres de datos de caché. Sin embargo, es interesante que la lastfailed funcionalidad lastfailed pueda funcionar con una sola entrada de caché. Nuestros datos de duración ocupan una entrada de caché para cada prueba. Sigamos el último ejemplo fallido y pongamos nuestros datos en un registro.


Leemos y almacenamos en caché para cada prueba. Podemos dividir el dispositivo en el dispositivo del alcance de la función para medir la duración y el dispositivo del alcance de la sesión para leer y escribir en el caché. Sin embargo, si hacemos esto, no podremos usar el dispositivo de caché porque tiene el alcance de la función. Afortunadamente, un vistazo rápido a la implementación en GitHub muestra que el dispositivo de almacenamiento en caché solo devuelve request.config.cache . Está disponible en cualquier área.


:


ch4/cache/test_slower_2.py


 Duration = namedtuple('Duration', ['current', 'last']) @pytest.fixture(scope='session') def duration_cache(request): key = 'duration/testdurations' d = Duration({}, request.config.cache.get(key, {})) yield d request.config.cache.set(key, d.current) @pytest.fixture(autouse=True) def check_duration(request, duration_cache): d = duration_cache nodeid = request.node.nodeid start_time = datetime.datetime.now() yield duration = (datetime.datetime.now() - start_time).total_seconds() d.current[nodeid] = duration if d.last.get(nodeid, None) is not None: errorstring = "test duration over 2x last duration" assert duration <= (d.last[nodeid] * 2), errorstring 

duration_cache . , , - . , namedtuple Duration current last . namedtuple test_duration , . , namedtuple , d.current . .


, :


 $ pytest -q --cache-clear test_slower_2.py ..... 5 passed in 2.80 seconds $ pytest -q --tb=no test_slower_2.py ...EE.. 7 passed, 2 error in 3.21 seconds $ pytest -q --cache-show -------------------------------- cache values --------------------------------- cache\lastfailed contains: {'test_slower_2.py::test_slow_stuff[2]': True, 'test_slower_2.py::test_slow_stuff[3]': True} duration\testdurations contains: {'test_slower_2.py::test_slow_stuff[0]': 0.483028, 'test_slower_2.py::test_slow_stuff[1]': 0.198011, 'test_slower_2.py::test_slow_stuff[2]': 0.426024, 'test_slower_2.py::test_slow_stuff[3]': 0.762044, 'test_slower_2.py::test_slow_stuff[4]': 0.056003, 'test_slower_2.py::test_slow_stuff[5]': 0.18401, 'test_slower_2.py::test_slow_stuff[6]': 0.943054} no tests ran in 0.02 seconds 

.


capsys


capsys builtin : stdout stderr , . stdout stderr.


, stdout:


ch4/cap/test_capsys.py

 def greeting(name): print('Hi, {}'.format(name)) 

, . - stdout. capsys:


ch4/cap/test_capsys.py

 def test_greeting(capsys): greeting('Earthling') out, err = capsys.readouterr() assert out == 'Hi, Earthling\n' assert err == '' greeting('Brian') greeting('Nerd') out, err = capsys.readouterr() assert out == 'Hi, Brian\nHi, Nerd\n' assert err == '' 

stdout stderr capsys.redouterr() . — , , .


stdout . , stderr :


 def yikes(problem): print('YIKES! {}'.format(problem), file=sys.stderr) def test_yikes(capsys): yikes('Out of coffee!') out, err = capsys.readouterr() assert out == '' assert 'Out of coffee!' in err 

pytest . print . . -s , stdout . , , . , pytest , , . capsys . capsys.disabled() , .


Aquí hay un ejemplo:


ch4/cap/test_capsys.py

 def test_capsys_disabled(capsys): with capsys.disabled(): print('\nalways print this') #    print('normal print, usually captured') #  ,   

, 'always print this' :


 $ cd /path/to/code/ch4/cap $ pytest -q test_capsys.py::test_capsys_disabled 

, always print this , capys.disabled() . print — print, normal print, usually captured ( , ), , -s , --capture=no , .


monkeypatch


"monkey patch" — . "monkey patching" — , , . monkeypatch . , , , , . , . API , monkeypatch .


monkeypatch :


  • setattr(target, name, value=<notset>, raising=True) : .
  • delattr(target, name=<notset>, raising=True) : .
  • setitem(dic, name, value) : .
  • delitem(dic, name, raising=True) : .
  • setenv(name, value, prepend=None) : .
  • delenv(name, raising=True) : .
  • syspath_prepend(path) : sys., Python.
  • chdir(path) : .

raising pytest, , . prepend setenv() . , + prepend + <old value> .


monkeypatch , , dot- . , dot- . , cheese- :


ch4/monkey/cheese.py

 import os import json def read_cheese_preferences(): full_path = os.path.expanduser('~/.cheese.json') with open(full_path, 'r') as f: prefs = json.load(f) return prefs def write_cheese_preferences(prefs): full_path = os.path.expanduser('~/.cheese.json') with open(full_path, 'w') as f: json.dump(prefs, f, indent=4) def write_default_cheese_preferences(): write_cheese_preferences(_default_prefs) _default_prefs = { 'slicing': ['manchego', 'sharp cheddar'], 'spreadable': ['Saint Andre', 'camembert', 'bucheron', 'goat', 'humbolt fog', 'cambozola'], 'salads': ['crumbled feta'] } 

, write_default_cheese_preferences() . , . , . .


, . , read_cheese_preferences() , , write_default_cheese_preferences() :


ch4/monkey/test_cheese.py

 def test_def_prefs_full(): cheese.write_default_cheese_preferences() expected = cheese._default_prefs actual = cheese.read_cheese_preferences() assert expected == actual 

, , , cheese- . .


HOME set , os.path.expanduser() ~ , HOME . HOME , :


ch4/monkey/test_cheese.py

 def test_def_prefs_change_home(tmpdir, monkeypatch): monkeypatch.setenv('HOME', tmpdir.mkdir('home')) cheese.write_default_cheese_preferences() expected = cheese._default_prefs actual = cheese.read_cheese_preferences() assert expected == actual 

, HOME . - expanduser() , , «On Windows, HOME and USERPROFILE will be used if set, otherwise a combination of….» . ! , Windows. , .


, HOME , expanduser :


ch4/monkey/test_cheese.py

 def test_def_prefs_change_expanduser(tmpdir, monkeypatch): fake_home_dir = tmpdir.mkdir('home') monkeypatch.setattr(cheese.os.path, 'expanduser', (lambda x: x.replace('~', str(fake_home_dir)))) cheese.write_default_cheese_preferences() expected = cheese._default_prefs actual = cheese.read_cheese_preferences() assert expected == actual 

, cheese os.path.expanduser() -. re.sub ~ . setenv() setattr() . , setitem() .


, , , . , , write_default_cheese_preferences() :


ch4/monkey/test_cheese.py

 def test_def_prefs_change_defaults(tmpdir, monkeypatch): #      fake_home_dir = tmpdir.mkdir('home') monkeypatch.setattr(cheese.os.path, 'expanduser', (lambda x: x.replace('~', str(fake_home_dir)))) cheese.write_default_cheese_preferences() defaults_before = copy.deepcopy(cheese._default_prefs) #     monkeypatch.setitem(cheese._default_prefs, 'slicing', ['provolone']) monkeypatch.setitem(cheese._default_prefs, 'spreadable', ['brie']) monkeypatch.setitem(cheese._default_prefs, 'salads', ['pepper jack']) defaults_modified = cheese._default_prefs #       cheese.write_default_cheese_preferences() #    actual = cheese.read_cheese_preferences() assert defaults_modified == actual assert defaults_modified != defaults_before 

_default_prefs - , monkeypatch.setitem() , .


setenv() , setattr() setitem() . del . , , - . monkeypatch .


syspath_prepend(path) sys.path , , . stub-. monkeypatch.syspath_prepend() , , stub-.


chdir(path) . , . , , monkeypatch.chdir(the_tmpdir) .


monkeypatch unittest.mock , . 7 " pytest " . 125.


doctest_namespace


doctest Python docstrings , , . pytest doctest Python --doctest-modules . doctest_namespace , autouse , pytest doctest . docstrings .


doctest_namespace , Python . , numpy import numpy as np .


. , unnecessary_math.py multiply() divide() , . , docstring , docstrings :


ch4/dt/1/unnecessary_math.py

 """ This module defines multiply(a, b) and divide(a, b). >>> import unnecessary_math as um Here's how you use multiply: >>> um.multiply(4, 3) 12 >>> um.multiply('a', 3) 'aaa' Here's how you use divide: >>> um.divide(10, 5) 2.0 """ def multiply(a, b): """ Returns a multiplied by b. >>> um.multiply(4, 3) 12 >>> um.multiply('a', 3) 'aaa' """ return a * b def divide(a, b): """ Returns a divided by b. >>> um.divide(10, 5) 2.0 """ return a / b 

unnecessary_math , um , import noecessary_math as um -. docstrings import , um . , pytest docstring . docstring , docstrings :


 $ cd /path/to/code/ch4/dt/1 $ pytest -v --doctest-modules --tb=short unnecessary_math.py ============================= test session starts ============================= collected 3 items unnecessary_math.py::unnecessary_math PASSED unnecessary_math.py::unnecessary_math.divide FAILED unnecessary_math.py::unnecessary_math.multiply FAILED ================================== FAILURES =================================== ______________________ [doctest] unnecessary_math.divide ______________________ 034 035 Returns a divided by b. 036 037 >>> um.divide(10, 5) UNEXPECTED EXCEPTION: NameError("name 'um' is not defined",) Traceback (most recent call last): ... File "<doctest unnecessary_math.divide[0]>", line 1, in <module> NameError: name 'um' is not defined ... _____________________ [doctest] unnecessary_math.multiply _____________________ 022 023 Returns a multiplied by b. 024 025 >>> um.multiply(4, 3) UNEXPECTED EXCEPTION: NameError("name 'um' is not defined",) Traceback (most recent call last): ... File "<doctest unnecessary_math.multiply[0]>", line 1, in <module> NameError: name 'um' is not defined /path/to/code/ch4/dt/1/unnecessary_math.py:23: UnexpectedException ================ 2 failed, 1 passed in 0.03 seconds ================= 

- import docstring:


ch4/dt/2/unnecessary_math.py

 """ This module defines multiply(a, b) and divide(a, b). >>> import unnecessary_math as um Here's how you use multiply: >>> um.multiply(4, 3) 12 >>> um.multiply('a', 3) 'aaa' Here's how you use divide: >>> um.divide(10, 5) 2.0 """ def multiply(a, b): """ Returns a multiplied by b. >>> import unnecessary_math as um >>> um.multiply(4, 3) 12 >>> um.multiply('a', 3) 'aaa' """ return a * b def divide(a, b): """ Returns a divided by b. >>> import unnecessary_math as um >>> um.divide(10, 5) 2.0 """ return a / b 

:


 $ cd /path/to/code/ch4/dt/2 $ pytest -v --doctest-modules --tb=short unnecessary_math.py ============================= test session starts ============================= collected 3 items unnecessary_math.py::unnecessary_math PASSED [ 33%] unnecessary_math.py::unnecessary_math.divide PASSED [ 66%] unnecessary_math.py::unnecessary_math.multiply PASSED [100%] ===================== 3 passed in 0.03 seconds ====================== 

docstrings .


doctest_namespace , autouse conftest.py , :


ch4/dt/3/conftest.py

 import pytest import unnecessary_math @pytest.fixture(autouse=True) def add_um(doctest_namespace): doctest_namespace['um'] = unnecessary_math 

pytest um doctest_namespace , unnecessary_math . conftest.py, doctests, conftest.py um .


doctest pytest 7 " pytest " . 125.


recwarn


recwarn , . Python , , , . , , , , . :


ch4/test_warnings.py

 import warnings import pytest def lame_function(): warnings.warn("Please stop using this", DeprecationWarning) # rest of function 

, :


ch4/test_warnings.py

 def test_lame_function(recwarn): lame_function() assert len(recwarn) == 1 w = recwarn.pop() assert w.category == DeprecationWarning assert str(w.message) == 'Please stop using this' 

recwarn , category (), message (), filename ( ) lineno ( ), .


. , , , . recwarn.clear() , , .


recwarn , pytest pytest.warns() :


ch4/test_warnings.py

 def test_lame_function_2(): with pytest.warns(None) as warning_list: lame_function() assert len(warning_list) == 1 w = warning_list.pop() assert w.category == DeprecationWarning assert str(w.message) == 'Please stop using this' 

pytest.warns() . recwarn pytest.warns() , , , .


Ejercicios


  1. ch4/cache/test_slower.py autouse , check_duration() . ch3/tasks_proj/tests/conftest.py .
  2. Realice las pruebas en el Capítulo 3.
  3. Para pruebas realmente muy rápidas, 2x realmente rápido sigue siendo muy rápido. En lugar de 2x, cambie el dispositivo para verificar durante 0.1 segundos más 2x para la última duración.
  4. Ejecute pytest con el dispositivo modificado. ¿Los resultados parecen razonables?

Que sigue


En este capítulo, viste algunos de los accesorios de Pytest incorporados. A continuación, considerará los complementos con más detalle. Los matices de escribir grandes complementos pueden convertirse en un libro en sí mismos; sin embargo, pequeños complementos personalizados son una parte regular del ecosistema pytest.


Atrás Siguiente

Source: https://habr.com/ru/post/448792/


All Articles