angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Prueba funcional de un sitio web multilingüe Flask con Pytest

Usando Pytest fixtures y hooks y Flask test_client podemos hacer pruebas y aprobar textos manualmente.

25 julio 2020
En Testing
post main image
https://unsplash.com/@marco_blackboulevard

La prueba es un trabajo duro. Es totalmente diferente de crear o modificar la funcionalidad. Lo sé, también desarrollé hardware de computadoras, circuitos integrados, sistemas de prueba de hardware. Escribí pruebas para CPUs, productos informáticos, desarrollé sistemas de prueba. Con el hardware no puedes cometer errores. Los errores pueden ser el fin de tu empresa.

Bienvenido al maravilloso mundo de las pruebas de software

Con el software muchas cosas son diferentes. Los errores son tolerados. No culpo a las pequeñas empresas. Pero los grandes gigantes de la tecnología como Google, Apple, Facebook, Microsoft debería ser culpado. La mayoría de los programas son complejos o al menos utilizan bibliotecas de terceros o se conectan a sistemas remotos. Probar una aplicación se ha convertido en una tarea compleja.

Por qué elijo escribir pruebas funcionales (primero)

Hay muchos tipos diferentes de pruebas para el software, por mencionar los más conocidos por los desarrolladores:

  • Prueba de la unidad
  • Prueba de integración
  • Prueba funcional
  • Prueba de extremo a extremo

Las pruebas unitarias son fáciles de escribir, pero incluso si todas pasan, esto no garantiza que mi aplicación Flask funcione. Las pruebas de integración tampoco son tan difíciles. Si escribes bibliotecas estas pruebas tienen sentido. Sólo tienes que tirar de las clases y escribir algunas pruebas. Pero no hay garantía de que mi aplicación funcione cuando sólo se hacen estas pruebas.

Con tiempo limitado, ¿cómo puedo asegurarme de que mi aplicación Flask CMS/Blog funciona realmente? Decidí hacer las pruebas funcionales y las pruebas de end-to-end . Esto es mucho más trabajo pero al menos sé que el visitante de la página web ve las cosas correctas.

Herramientas de prueba

Añadí una opción de "prueba" a mi configuración de la plataforma. Esta es como mi opción de 'desarrollo', con código fuera del contenedor, pero con algunos paquetes extra,

  • Flake8
  • Pytest
  • Pytest-cov
  • Pytest-flake8

Pytest es una buena prueba framework si quiere probar su aplicación Flask . Cuando instale Pytest-flake8 también puede ejecutar Flake8 desde Pytest:

python3 -m pytest  app_frontend/blueprints/demo  --flake8  -v

Pero prefiero ejecutar flake8 por sí mismo, en este caso ignorando las líneas largas:

flake8  --ignore=E501  app_frontend/blueprints/demo

Configuración de la prueba

Empezar con Pytest no es muy difícil. Creé un directorio tests_app_frontend en el directorio del proyecto:

.
|-- alembic
|--  app_admin
|--  app_frontend
|   |--  blueprints
|   |-- templates
|   |-- translations
|   |-- __init__.py
|   `-- ...
|-- shared
|-- tests_frontend
|   |-- helpers
|   |   |-- helpers_cleanup.py
|   |   |-- helpers_globals.py
|   |   `-- helpers_utils.py
|   |-- language_logs
|   |-- language_logs_ok
|   |-- conftest.py
|   |--  pytest.ini
|   |-- ...
|   |-- test_ay_account.py
|   |-- ...
|   |-- test_an_auth_login.py
|   `-- ...

El archivo pytest.ini :

[pytest]
norecursedirs=tests_frontend/helpers

cache_dir=/home/flask/project/tests_frontend/cache
console_output_style=progress

log_file=/home/flask/project/tests_frontend/testresults_frontend.log
log_file_level=DEBUG
log_file_format=%(asctime)s %(levelname)s %(message)s
log_file_date_format=%Y-%m-%d %H:%M:%S

log_level=DEBUG
python_files=test_*.py

flake8-max-line-length=200

El directorio de ayudantes contiene funciones que pueden hacer tu vida mucho más fácil, de hecho, después de escribir algunas pruebas, estaba buscando minimizar mi código de pruebas, y pasé mucho tiempo escribiendo estas funciones de ayuda.

Las funciones de limpieza en helpers_cleanup.py son parte de la prueba y se ejecutan antes de la prueba real, ejemplos:

def delete_test_contact_forms_from_db(client=None,  lang_code=None, dbg=False):
    ...
    
def delete_test_contact_form_emails_from_db(client=None,  lang_code=None, dbg=False):
    ...

Algunos de los globos almacenados en helpers.globals:

# all test emails have the same email_host
email_host = 'test_email_host.test'

# test  user  present in the database
my_test_user_email = 'mytestuser@mytestuser.mytestuser'
my_test_user_password = 'nio7FKCYUHTgd765edfglfsSDgn'
my_test_user_new_password = 'LUI76tlfvutjlFLJFgf'

Algunas de las funciones de ayuda en helper_utils.py:

def get_attr_from_html_page_by_elem_id(rv, elem_id, attr_name, form_prefix=None, dbg=False):
    ...

def get_unique_test_email():
    ...

def page_has_tag_with_text(rv, tag, text, dbg=False):
    ...

def client_get(client, url, follow_redirects=False, dbg=False):
    ...

def client_post(client, url, form_field2values=None, form_prefix=None, follow_redirects=False, dbg=False):
    ...

def my_test_user_restore_password(dbg=False):
    ...

def my_test_user_login(client,  lang_code=None, email=None, password=None, check_message_logged_in=True, dbg=False):
    ...

def my_test_user_logout(client,  lang_code=None, check_message_logged_out=True, dbg=False):
    ...

def get_auth_login_url(client=None,  lang_code=None, dbg=False):
    ...

def get_auth_logout_url(client=None,  lang_code=None, dbg=False):
    ...

Añadí una función client_get() y client_post() que se comportan casi idénticamente a client.get() y client.post() pero también devuelven la ubicación de la página después de las redirecciones. La función client_post() también tiene un parámetro opcional form_prefix que puede ser usado cuando el formulario es prefixed, ver documentación WTForms .

La función my_test_user_login() puede ser usada cuando no está autentificada para iniciar la sesión de la prueba user. Todos los archivos de ayuda tienen un argumento opcional dbg (debug). Cuando se desarrollan pruebas a menudo se quiere ver lo que está pasando, establecer dbg=True me da todos los detalles, también en el archivo de registro.

Los nombres de los archivos de prueba, los nombres de las funciones y la autenticación

En Pytest puedes usar 'mark' para agrupar pruebas pero yo no he usado esto todavía. En su lugar tengo dos categorías principales. Prueba cuando el user no está autenticado, an = no autenticado, y prueba cuando el user está autenticado, ay = autenticado. Los archivos de prueba y las pruebas comienzan con la misma cadena. Archivo de prueba:

test_an_auth_login.py

tiene pruebas como:

def test_an_auth_login_enable_disable(client_lang_code_an):
    client,  lang_code  =  client_lang_code_an
    ...

Y el archivo de prueba:

test_ay_account.py

tiene pruebas como:

def test_ay_account_change_password_to_new_password(client_lang_code_ay):
    client,  lang_code  =  client_lang_code_ay
    ...

Los parámetros 'client_lang_code_an' y 'client_lang_code_ay' son Tuples que contienen el cliente (de prueba) y el código del idioma.
Están desempaquetados en la función de prueba. Hay un paquete Pytest-cases que evita el desempaquetado, pero no quería usarlo.

Iniciando las pruebas con fixtures

Quiero que las pruebas se ejecuten una a una para todos los idiomas disponibles, o sólo uno o más idiomas que especifique en la línea de comandos. Esto significa que el ámbito fixture debe ser "función". Ejemplo de salida de Pytest:

tests_frontend/test_ay_account.py::test_ay_account_change_password_logout_login[de] PASSED
tests_frontend/test_ay_account.py::test_ay_account_change_password_logout_login[en] PASSED
tests_frontend/test_ay_account.py::test_ay_account_change_password_logout_login[es] PASSED
tests_frontend/test_ay_account.py::test_ay_account_change_password_logout_login[fr] PASSED
tests_frontend/test_an_auth_login.py::test_an_auth_login_email_too_many_attempts[de] PASSED
tests_frontend/test_an_auth_login.py::test_an_auth_login_email_too_many_attempts[en] PASSED
tests_frontend/test_an_auth_login.py::test_an_auth_login_email_too_many_attempts[es] PASSED
tests_frontend/test_an_auth_login.py::test_an_auth_login_email_too_many_attempts[fr] PASSED

Se podría argumentar que hacer primero todas las pruebas para el lenguaje X y luego todas las pruebas para el lenguaje Y, es más rápido. Tienes razón, ver también abajo "cambiando el alcance de Pytest ", pero yo quería la opción de mantener las cosas juntas cuando se ejecutaban múltiples pruebas nuevas. Aquí está la parte principal de conftest.py:

from  app_frontend  import  create_app


def pytest_addoption(parser):
    parser.addoption('--lc', action='append', default=[],
        help='lc (one or more possible)')


def pytest_generate_tests(metafunc):
    default_opts = ['de', 'en', 'es']
	lc_opts = metafunc.config.getoption('lc') or default_opts
    if 'client_lang_code_an' in metafunc.fixturenames:
        metafunc.parametrize('client_lang_code_an', lc_opts, indirect=True)
    if 'client_lang_code_ay' in metafunc.fixturenames:
        metafunc.parametrize('client_lang_code_ay', lc_opts, indirect=True)


@pytest.fixture(scope='function')
def  client_lang_code_an(request):

     lang_code  = request.param
    project_config = os.getenv('PROJECT_CONFIG')
    flask_app =  create_app(project_config)

    with flask_app.test_client() as client:
        with flask_app.app_context():
            rv = client.get(get_language_root_url(lang_code))
            yield (client,  lang_code)


@pytest.fixture(scope='function')
def  client_lang_code_ay(request):

     lang_code  = request.param
    project_config = os.getenv('PROJECT_CONFIG')
    flask_app =  create_app(project_config)

    with flask_app.test_client() as client:
        with flask_app.app_context():
            rv = client.get(get_language_root_url(lang_code))
            my_test_user_restore_password()
            my_test_user_login(client,  lang_code, change_to_language=True)
            yield (client,  lang_code)
            my_test_user_logout(client)

No hay mucho especial aquí. Las pruebas que deben ejecutarse con un user autentificado tienen el parámetro 'client_lang_code_ay'. Esto se refiere al fixture con este nombre. En este caso, el cliente se conecta antes de que se llame la prueba.

Si no especifico el '--lc' en la línea de comandos, se utilizan todos los idiomas para una prueba. Puedo seleccionar uno o más idiomas especificando, por ejemplo, '--lc de' en la línea de comandos. De hecho, más tarde también añadí algunos que me permiten escribir '--lc=fr,en,de'.

Base de datos

Debido a que esta es una prueba funcional de un CMS/Blog necesito algunos datos en la base de datos. Antes de una prueba, debo limpiar algunos datos para evitar interferencias con la prueba. Debo escribir este código de limpieza yo mismo. Esto es un trabajo extra pero no tan malo porque puedo usar partes de este código también en la aplicación más tarde. Como cuando se quita un user.

Algunas pruebas

Cuando no está autentificada la situación no es muy compleja. Aquí hay una prueba que comprueba el mensaje de error con una contraseña demasiado corta.

def test_an_auth_login_password_too_short(client_lang_code_an):
    client,  lang_code  =  client_lang_code_an
    """
    TEST: submit login_form with too short password
    EXPECT: proper error message
    """

    cleanup_db_auth()

    auth_login_url = get_auth_login_url()

    auth_login_info = AuthLoginInfo()
    login_form_prefix  = auth_login_info.login_form_prefix

    input_validation = InputValidationAuth()
    length_min = input_validation.password.length_min
    error_message = check_text(lang_code, input_validation.password.error_message_length_min)

    url, rv = client_post(
        client,
        auth_login_url,
        form_field2values=get_login_form_form_field2values_for_submit(
            password='x' * (length_min - 1)
        ),
        form_prefix=login_form_prefix,
        follow_redirects=True
    )
    assert page_contains_text(rv, error_message)

Antes de la prueba real se eliminan algunos datos de la base de datos, si están presentes, para evitar interferencias. Obtengo la forma prefix, la longitud mínima y el mensaje de error tirando de algunas clases.

Se vuelve más complejo cuando se autentica. Esto tiene que ver con el hecho de que un user registrado puede establecer el idioma preferido en la cuenta. Por ejemplo, el user con el alemán (Deutsch) como idioma preferido, está en la versión inglesa del sitio, y luego se conecta. Después de iniciar la sesión, el idioma del sitio se cambia al alemán (Deutsch) automáticamente.

Como quiero comprobar los mensajes en todos los idiomas, he añadido un código a la función my_test_user_login() para hacer mi vida más fácil. Si el idioma, lang_code, está especificado, entonces después de un inicio de sesión el user se mueve a la versión del idioma especificado.

def test_ay_auth_try_to_login_when_loggedin_get(client_lang_code_ay):
    client,  lang_code  =  client_lang_code_ay
    """
    TEST: logged in test  user, try to login
    EXPECT: proper error message
    """

    cleanup_db_auth()

    auth_login_url = get_auth_login_url()

    auth_login_info = AuthLoginInfo()
    error_message = check_text(lang_code, auth_login_info.error_message_you_are_already_logged_in)

    rv = client.get(auth_login_url, follow_redirects=True)
    assert page_contains_text(rv, error_message), 'not found, error_message = {}'.format(error_message)

Tiempo de prueba

Para cronometrar una prueba puedes simplemente poner el comando de tiempo delante del comando Pytest , por ejemplo:

time python3 -m pytest --cov=shared/blueprints/auth --cov-report term-missing  tests_frontend  -k "test_ay_auth_try_to_login_when_loggedin_get" -s -v

imprime al final a la terminal:

real	0m 16.67s
user	0m 9.65s
sys	0m 0.29s

Este es el resultado de una sola prueba con seis idiomas, un promedio de 2,5 segundos por idioma.

Una forma más fácil de ver en detalle los tiempos de ejecución de las pruebas es añadiendo el parámetro --durations=N a la línea de comandos. Si N=0 se obtienen los tiempos de ejecución de cada prueba. Con N=1 se obtiene el tiempo de ejecución de la prueba más lenta. Con N=2 se obtiene el tiempo de ejecución de las dos pruebas más lentas, etc. Por ejemplo, para obtener las 4 pruebas más lentas de la forma de contacto el comando es:

time python3 -m pytest --cov=shared/blueprints/auth --cov-report term-missing  tests_frontend  -k "test_an_contact" -s -vv  --durations=4 --lc=fr

El resultado incluye:

========== slowest 4 test durations ==========
14.88s call     test_an_contact_form.py::test_an_contact_form_multiple_same_ip_address[fr]
6.96s call     test_an_contact_form.py::test_an_contact_form_multiple_same_email[fr]
6.36s call     test_an_contact_form.py::test_an_contact_form_missing_form_field[fr]
2.87s call     test_an_contact_form.py::test_an_contact_form_thank_you_logged_in[fr]
========== 15 passed, 24 deselected, 1 warning in 51.92s ==========

Los tiempos dentro del fixture

Para ver a dónde va todo este tiempo hice una prueba de tiempo recolectando tiempos en varias etapas en el autentificado fixture, client_lang_code_ay().

0.0  client_lang_code_ay
0.141031 after: flask_app =  create_app(project_config)
0.141062 after: with flask_app.test_client() as client
0.141078 after: with flask_app.app_context()
0.448304 after: rv = client.get(get_language_root_url(lang_code))
0.531201 after: my_test_user_restore_password()
1.154249 after: my_test_user_login(client,  lang_code)
2.553381 after: yield (client,  lang_code)
2.624607 after: my_test_user_logout(client,  lang_code)

El tiempo de inicio de sesión es más largo de lo que se puede esperar, esto es causado por un retraso deliberado en el inicio de sesión. Las pruebas con el no autentificado fixture, client_lang_code_an(), toman un promedio de 1.5 segundos, lo que se espera sin el inicio de sesión.

Reducir el tiempo de la prueba cambiando el alcance de Pytest

Debido a que el ámbito Pytest fixture es 'función', obtenemos un nuevo test_client para cada prueba. Usé el scope='function' para ver las pruebas ejecutadas para todos los idiomas. La salida de Pytest:

test1[de] PASSED
test1[en] PASSED
test1[es] PASSED
test1[fr] PASSED
test2[de] PASSED
test2[en] PASSED
test2[es] PASSED
test2[fr] PASSED
test3[de] PASSED
test3[en] PASSED
test3[es] PASSED
test3[fr] PASSED

Esto es muy bonito pero también lleva mucho tiempo.

Para reducir el tiempo de la prueba cambio el alcance de fixture a "sesión". En este caso se creará un nuevo test_client sólo para cada idioma. Primero se ejecutan todas las pruebas para el lenguaje 'de', luego se ejecutan todas las pruebas para el lenguaje 'en', etc.. La salida de Pytest:

test1[de] PASSED
test2[de] PASSED
test3[de] PASSED
test1[en] PASSED
test2[en] PASSED
test3[en] PASSED
test1[es] PASSED
test2[es] PASSED
test3[es] PASSED
test1[fr] PASSED
test2[fr] PASSED
test3[fr] PASSED

El tiempo de prueba se reduce dramáticamente de esta manera. Pero sólo si se seleccionan muchas, todas, las pruebas, por supuesto. En general, la mayoría de las pruebas se completarán muy rápido y sólo unos pocos son responsables del 80% del tiempo de la prueba.

El tiempo de prueba para 14 pruebas (contact_form) usando el scope='function' fue de 00:06:11. Después de cambiar el scope a 'sesión' el tiempo fue de 00:04:56. En este caso la reducción del tiempo de prueba es del 20%. En otro caso la reducción fue de más del 50%.

Diseño para la prueba

Cuantos más datos podamos obtener de nuestra aplicación, más podremos automatizar las pruebas. Por ejemplo, ¿cuál es el prefix del formulario de ingreso? ¿Cuáles son los mensajes de error cuando se intenta iniciar la sesión cuando ya se ha iniciado la sesión? ¿Cuál es el número mínimo y máximo de caracteres para una contraseña? Estos datos están presentes en la aplicación, ¿cómo los sacamos?

Para algunas clases añadí una clase "Info" que es el padre de la clase real.

class AuthLoginInfo:

    def __init__(self):

        self.error_message_you_are_already_logged_in = _('You are already logged in.')
        self.error_message_login_is_temporary_suspended = _('Log in is temporary suspended.')
        ...
        self.message_you_are_logged_in = _('You are logged in.')

        self.login_form_prefix  = self.login_form_id


class AuthLogin(AuthLoginInfo):

    def __init__(self):
        super().__init__()

    def login(self):

        if already_logged_in():
            flash(self.error_message_you_are_already_logged_in, 'error')
            return redirect(already_logged_in_redirect_url())
        ...

Todavía no sé si esto se puede hacer mejor, pero es factible.

Automatización de la comprobación del lenguaje

En las pruebas anteriores no sabemos si los mensajes son realmente correctos y traducidos para cada idioma. Debemos revisar manualmente los textos pero podemos intentar minimizar nuestro trabajo.

He creado una función check_text() que hace tres cosas:

  • Almacenar el texto en un diccionario
  • Comprueba si el mismo texto ya está en un diccionario
  • Almacenar el texto en un archivo de registro de idioma

Los nombres de estos archivos de registro de idioma son los nombres de las pruebas. Se ejecuta una prueba para diferentes idiomas. Si el texto ya está en el diccionario, entonces probablemente algo está mal, como que el texto no está traducido. En este caso llamo a "assert" en la función check_text(). Esto muestra un error para que podamos investigar qué es lo que está mal.

Si todo va bien, compruebo el archivo de registro del idioma para ver los textos. Los campos de líneas separados por tabulaciones en este archivo son test_name, text_id, lang_code, text:

test_ay_auth_try_to_login_when_loggedin_get	default	de	Sie sind bereits eingeloggt.
test_ay_auth_try_to_login_when_loggedin_get	default	en	You are already logged in.
test_ay_auth_try_to_login_when_loggedin_get	default	es	Ya está conectado.
test_ay_auth_try_to_login_when_loggedin_get	default	fr	Vous êtes déjà connecté.
test_ay_auth_try_to_login_when_loggedin_get	default	nl	Je bent al ingelogd.
test_ay_auth_try_to_login_when_loggedin_get	default	ru	Вы уже вошли в систему.

No sé ruso y sólo un poco de español y francés, pero creo que esto se ve bien.

Más sobre la función check_text()

Puedo especificar un text_id cuando quiero comprobar más de un texto en una función de prueba. Si no lo especificamos, se utiliza el "por defecto".
También he añadido un indicador para los textos que no queremos comprobar (automáticamente). El nombre de la prueba, el llamador, se extrae de la pila. Las primeras líneas de la función check_text() :

def check_text(lang_code, text, text_id=None, do_not_check=False, dbg=False):
    test_name = inspect.stack()[1][3]
    ...

Hemos hecho mucho, pero todavía tenemos que comprobar manualmente los textos cada vez después de una prueba. Esto es todavía demasiado trabajo, hemos verificado los textos, las traducciones, en el archivo de registro del idioma y no queremos hacer esto de nuevo!

Para evitar esto he creado un directorio 'language_logs_ok' y copio aquí los archivos de registro del idioma que hemos aprobado. Pero aún así es demasiado trabajo, ¡no queremos copiar los archivos manualmente!

Con un poco de código extra podemos comprobar al final de una prueba si este ok_file existe. Si no existe, podemos pedir que se aprueben los textos y si la respuesta es afirmativa, copiamos el archivo de registro del idioma también en el directorio 'language_logs_ok'. Si existe podemos comparar los textos de la prueba actual con los textos aprobados y señalar un error si no coinciden.

Pytest tiene hooks que puede ser usado para añadir funcionalidad antes del comienzo de las pruebas y después de que las pruebas hayan terminado. Aquí sólo uso el último:

def pytest_sessionfinish(session, exitstatus):
    ....

He añadido el código y ahora el resultado es:

test_an_auth_login_password_too_long: NO ok file yet WARNING
texts:
test_an_auth_login_password_too_long	default	de	Das Passwort muss zwischen 6 und 40 Zeichen lang sein.
test_an_auth_login_password_too_long	default	en	Password must be between 6 and 40 characters long.
test_an_auth_login_password_too_long	default	es	La contraseña debe tener entre 6 y 40 caracteres.
test_an_auth_login_password_too_long	default	fr	Le mot de passe doit comporter entre 6 et 40 caractères.
test_an_auth_login_password_too_long	default	nl	Het wachtwoord moet tussen 6 en 40 tekens lang zijn.
test_an_auth_login_password_too_long	default	ru	Пароль должен иметь длину от 6 до 40 символов.
Do you approve these texts (create an ok file)? [yN]

Si apruebo estos textos, se crea el archivo ok_file. Luego, en la siguiente ejecución, después de los resultados de la prueba Pytest se muestra el siguiente mensaje:

test_an_auth_login_password_too_long: language match with ok_file PASSED

¡Esto no sólo quita mucho estrés, sino también mucho tiempo!

Otra prueba con múltiples textos

Estoy usando el paquete WTForms . Puedo meter el formulario en la prueba, extraer las etiquetas de los campos del formulario y comprobar que están en la página. Al automatizar las comprobaciones de las etiquetas de los campos de formulario eliminamos las traducciones perdidas o erróneas.

El formulario de acceso:

class AuthLoginForm(FlaskForm):

    email = StringField(
        _l('Email'),
        filters=[strip_whitespace],
        validators=[InputRequired()])

    password = PasswordField(
        _l('Password'),
        render_kw={'autocomplete': 'off'},
        filters=[strip_whitespace],
        validators=[InputRequired()])

    remember = BooleanField(_l('Remember me'))

    submit = SubmitField(_l('Log in'))

La prueba:

def test_an_auth_login_form_labels(client_lang_code_an):
    client,  lang_code  =  client_lang_code_an
    """
    TEST: check login form labels
    EXPECT: proper error message
    """
    fname = get_fname()

    cleanup_db_auth()

    auth_login_url = get_auth_login_url()

    auth_login_info = AuthLoginInfo()
    auth_login_form_prefix  = auth_login_info.login_form_prefix

    auth_login_form = AuthLoginForm(prefix=auth_login_form_prefix)

    # select page
    location, rv = client_get(client, auth_login_url, follow_redirects=True)

    assert page_contains_form_field_label(rv, 'email', auth_login_form,  lang_code, form_prefix=auth_login_form_prefix)
    assert page_contains_form_field_label(rv, 'password', auth_login_form,  lang_code, form_prefix=auth_login_form_prefix)
    assert page_contains_form_field_label(rv, 'remember', auth_login_form,  lang_code, form_prefix=auth_login_form_prefix)
    assert page_contains_form_field_label(rv, 'submit', auth_login_form,  lang_code, form_prefix=auth_login_form_prefix)

Y el resultado después de una prueba exitosa:

tests_frontend/test_an_auth_login.py::test_an_auth_login_form_labels[de] PASSED
tests_frontend/test_an_auth_login.py::test_an_auth_login_form_labels[en] PASSED
tests_frontend/test_an_auth_login.py::test_an_auth_login_form_labels[es] PASSED
tests_frontend/test_an_auth_login.py::test_an_auth_login_form_labels[fr] PASSED
tests_frontend/test_an_auth_login.py::test_an_auth_login_form_labels[nl] PASSED
tests_frontend/test_an_auth_login.py::test_an_auth_login_form_labels[ru] PASSED
test_an_auth_login_form_labels: language NO ok file yet WARNING
texts:
test_an_auth_login_form_labels	email	de	E-Mail
test_an_auth_login_form_labels	email	en	Email
test_an_auth_login_form_labels	email	es	Correo electrónico
test_an_auth_login_form_labels	email	fr	Courriel
test_an_auth_login_form_labels	email	nl	E-mail
test_an_auth_login_form_labels	email	ru	Электронная почта
test_an_auth_login_form_labels	password	de	Passwort
test_an_auth_login_form_labels	password	en	Password
test_an_auth_login_form_labels	password	es	Contraseña
test_an_auth_login_form_labels	password	fr	Mot de passe
test_an_auth_login_form_labels	password	nl	Wachtwoord
test_an_auth_login_form_labels	password	ru	Пароль
test_an_auth_login_form_labels	remember	de	Erinnern Sie sich an mich
test_an_auth_login_form_labels	remember	en	Remember me
test_an_auth_login_form_labels	remember	es	Recuérdame
test_an_auth_login_form_labels	remember	fr	Se souvenir de moi
test_an_auth_login_form_labels	remember	nl	Herinner mij
test_an_auth_login_form_labels	remember	ru	Помните меня
test_an_auth_login_form_labels	submit	de	Einloggen
test_an_auth_login_form_labels	submit	en	Log in
test_an_auth_login_form_labels	submit	es	Iniciar sesión
test_an_auth_login_form_labels	submit	fr	Ouvrir une session
test_an_auth_login_form_labels	submit	nl	Aanmelden
test_an_auth_login_form_labels	submit	ru	Войти
Do you approve these texts (create an ok file)? [yN] 

Cobertura de pruebas con Pytest-cov

También instalé Pytest-cov para obtener detalles sobre la cantidad de código que se utilizó durante una prueba. De nuevo, esto es muy valioso cuando se prueba una biblioteca, un paquete. Obtienes los números de línea del código que no fue usado en las pruebas. ¡Deberías obtener una cobertura del 100%!

Cuando se hacen pruebas funcionales, es inicialmente muy difícil alcanzar este 100% porque estamos probando partes de la aplicación. Por ejemplo, si tenemos una clase de validación de entrada, al comenzar con nuestras pruebas funcionales, podemos saltarnos muchos métodos de validación de entrada.

A veces, añado código extra para volver a comprobar si un valor es correcto. Esto se debe a que quiero asegurarme de que no he cometido un error en algún lugar, y también a veces no confío en un paquete de terceros que estoy usando. Este código no ha sido probado. Necesito añadir hooks para que este código sea comprobable... algún día.

Pruebas paralelas

Cuando puedo hacer pruebas en paralelo, el lote termina más rápido. Ya he hablado de los retrasos deliberados que se insertan al iniciar la sesión. Qué pérdida de tiempo de prueba. Pero no puedo ejecutar todas las pruebas al mismo tiempo. Algunas pruebas modifican el test_user, otras pueden interferir con las limitaciones de envío de correo electrónico, etc. Debo seleccionar cuidadosamente qué pruebas son independientes unas de otras. El paquete Pytest-xdist puede ayudar. Lo investigaré uno de estos días.

Problemas

Al comenzar con mi primer Pytest fixture ejecutando el Flask test_client obtuve el siguiente error:

AssertionError: a  localeselector  function is already registered

Flask-Babel con Pytest da AssertionError: una función localeselector ya está registrada. Con el primer parámetro ('de') no hay problemas, pero con el siguiente ('en', 'es', ...) se muestra el mensaje de error anterior. Esto sucede cuando se crea una nueva test_client .

Mi solución, que parece funcionar bien, es usar una variable global que indica si el localeselector ya estaba decorado. Si no, lo decoraré yo mismo.

    # removed @babel.localeselector
    def get_locale():
        do something

    global babel_localeselector_already_registered
    if not babel_localeselector_already_registered:
        get_locale = babel.localeselector(get_locale)
        babel_localeselector_already_registered = True

Resumen

Esta es mi primera implementación de pruebas principalmente funcionales para este sitio web multilingüe de CMS/Blog Flask . Me llevó bastante tiempo configurar y escribir las funciones de ayuda. Pero estas son esenciales para escribir pruebas compactas. Y estoy seguro de que tendré que escribir nuevas funciones de ayuda para nuevas categorías de pruebas.

No quiero codificar textos y parámetros en las pruebas. Los textos y parámetros pueden cambiar durante la vida útil de un producto. Cambié algunos códigos y añadí clases de información para poder introducir los textos y parámetros que quiero probar.

Las pruebas con textos requieren interacción y deben ser aprobadas manualmente. Después de la aprobación puedo hacer las mismas pruebas y los textos se compararán automáticamente con los textos aprobados. Si los textos cambian, se presenta un ERROR y se nos pedirá de nuevo que aprobemos estos cambios.

También implementé algunas pruebas 30+ con Selenium. Pero decidí centrarme primero en el Pytest + Flask .

Todavía hay mucho que desear, pero primero déjame escribir algo más de código y pruebas...

Enlaces / créditos

Create and import helper functions in tests without creating packages in test directory using py.test
https://stackoverflow.com/questions/33508060/create-and-import-helper-functions-in-tests-without-creating-packages-in-test-di

how to testing flask-login? #40
https://github.com/pytest-dev/pytest-flask/issues/40

Pytest - How to override fixture parameter list from command line?
https://stackoverflow.com/questions/51992562/pytest-how-to-override-fixture-parameter-list-from-command-line

pytest cheat sheet
https://gist.github.com/kwmiebach/3fd49612ef7a52b5ce3a

Python Pytest assertion error reporting
https://code-maven.com/pytest-assert-error-reporting

Testing Flask Applications
https://flask.palletsprojects.com/en/1.1.x/testing/

Deje un comentario

Comente de forma anónima o inicie sesión para comentar.

Comentarios (1)

Deje una respuesta.

Responda de forma anónima o inicie sesión para responder.

avatar

Thank you for such a thorough setup. I'll be copying things from here for sure!