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

Функциональное тестирование мультиязычного сайта Flask с Pytest

Используя Pytest fixtures и hooks и Flask test_client мы можем запускать тесты и вручную утверждать тексты.

25 июля 2020
В Testing
post main image
https://unsplash.com/@marco_blackboulevard

Тестирование - это тяжелая работа. Она полностью отличается от создания или модификации функциональности. Я знаю, я также разработал компьютерное оборудование, интегральные схемы, системы тестирования аппаратуры. Я написал тесты для CPUs, компьютерные продукты, разработал тестовые системы. С аппаратным обеспечением нельзя допускать ошибок. Ошибки могут стать концом Вашей компании.

Добро пожаловать в удивительный мир тестирования программного обеспечения

С программным обеспечением многие вещи отличаются. Ошибки допускаются. Я не виню маленькие компании. Но винить в этом должны такие крупные технические гиганты, как Google, Apple, Facebook, Microsoft. Большинство программ сложны или, по крайней мере, используют библиотеки сторонних производителей или подключаются к удаленным системам. Тестирование приложения стало сложной задачей.

Почему я решил написать функциональные тесты (в первую очередь).

Существует множество различных типов тестов для программного обеспечения, в частности, наиболее известные у разработчиков:

  • Юнит-тест
  • Интеграционный тест
  • Функциональный тест
  • Комплексный тест

Юнит-тесты легко писать, но даже если они все пройдут, это не гарантирует, что мое приложение Flask работает. Интеграционные тесты также не так уж и сложны. Если вы пишете библиотеки, то эти тесты имеют прекрасный смысл. Просто потяните за классы и напишите несколько тестов. Но нет никакой гарантии, что мое приложение будет работать только при выполнении этих тестов.

Как с помощью ограниченного времени убедиться, что мое приложение Flask CMS/Blog действительно работает? Я решил попробовать функциональные тесты и тесты end-to-end . Это гораздо больше работы, но, по крайней мере, я знаю, что посетитель сайта видит правильные вещи.

Инструменты для тестирования

Я добавил опцию "тестирования" в мою установку докеров. Это похоже на мою опцию 'разработки', с кодом вне контейнера, но с несколькими дополнительными пакетами,

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

Pytest - это хороший тест framework , если вы хотите протестировать ваше приложение Flask . При установке Pytest-flake8 вы также можете запустить Flake8 из Pytest:

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

Но я предпочитаю запускать flake8 самостоятельно, в данном случае игнорируя длинные строки:

flake8  --ignore=E501  app_frontend/blueprints/demo

Испытательная установка

Начиная с Pytest не очень сложно. Я создал каталог tests_app_frontend в каталоге проекта:

.
|-- 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
|   `-- ...

Файл 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

Каталог Helers содержит функции, которые могут сделать вашу жизнь намного проще, на самом деле после написания нескольких тестов, я смотрел на минимизацию своего тестового кода, и потратил много времени на написание этих вспомогательных функций.

Функции очистки в helpers_cleanup.py являются частью теста и запускаются перед фактическим тестом, примеры:

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):
    ...

Некоторые из глобусов хранятся в 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'

Некоторые вспомогательные функции в 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):
    ...

Я добавил функцию client_get() и client_post() , которые ведут себя практически идентично client.get() и client.post() , но также возвращают расположение страницы после переадресации. Функция client_post() также имеет дополнительный параметр формы_prefix , который можно использовать, когда форма prefixed, см. документацию WTForms .

Функцию my_test_user_login() можно использовать в случае отсутствия аутентификации для входа в тест user. Все вспомогательные файлы имеют необязательный аргумент dbg (отладка). При разработке тестов вы часто хотите посмотреть, что происходит, установка dbg=True дает мне все подробности, в том числе и в лог-файле.

Имена тестовых файлов, имена функций и аутентификация

В Pytest можно использовать 'mark' для групповых тестов, но я этого еще не делал. Вместо этого у меня есть две основные категории. Тестирует, когда user не аутентифицирован, a = не аутентифицирован, и тестирует, когда user аутентифицирован, ay = аутентифицирован. Тестовые файлы и тесты начинаются с одной и той же строки. Файл теста:

test_an_auth_login.py

имеет такие тесты:

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

И файл теста:

test_ay_account.py

есть такие тесты:

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

Параметры 'client_lang_code_an' и 'client_lang_code_ay' - это Tuples , которые содержат (тестовый) клиент и код языка.
Они распаковываются в функции теста. Есть пакет Pytest-cases , который позволяет избежать распаковки, но я не хотел его использовать.

Запуск тестов с fixtures

Я хочу, чтобы тесты выполнялись по одному для всех доступных языков, или только для одного или нескольких языков, которые я указываю в командной строке. Это означает, что область видимости fixture должна быть 'function'. Пример вывода из 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

Вы можете утверждать, что сначала запустить все тесты для языка X, а затем запустить все тесты для языка Y, быстрее. Вы правы, см. также ниже 'изменение области видимости Pytest ', но я хотел, чтобы при запуске нескольких новых тестов все оставалось как есть. Вот основная часть 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)

Здесь не так уж и много чего особенного. Тесты, которые должны запускаться с аутентифицированным user , имеют параметр 'client_lang_code_ay'. Это относится к fixture с таким именем. В этом случае клиент входит в систему перед вызовом теста.

Если я не укажу '--lc' в командной строке, то для теста будут использоваться все языки. Я могу выбрать один или несколько языков, указав в командной строке, например, '--lc de'. На самом деле позже я также добавил некоторые, которые позволяют мне набрать '--lc=fr,en,de'.

База данных

Поскольку это функциональный тест CMS/Blog, мне нужны некоторые данные в базе данных. Перед тестом я должен очистить некоторые данные, чтобы предотвратить вмешательство в тест. Я должен написать этот код очистки самостоятельно. Это дополнительная работа, но не так уж и плохо, потому что часть этого кода я могу использовать и в приложении позже. Как при удалении user.

Некоторые тесты

Когда не аутентифицирован, ситуация не очень сложная. Вот тест, который проверяет сообщение об ошибке со слишком коротким паролем.

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)

Перед фактическим тестированием некоторые данные удаляются из базы данных, если они присутствуют, чтобы избежать помех. Я получаю форму prefix, минимальную длину и сообщение об ошибке, потянув за некоторые классы.

При аутентификации все усложняется. Это связано с тем, что зарегистрированный user может установить предпочтительный язык в аккаунте. Например, user с немецким (Deutsch) в качестве предпочтительного языка, находится на английской версии сайта, а затем входит в систему. После входа на сайт язык сайта автоматически переключается на немецкий (Deutsch).

Так как я хочу проверять сообщения на всех языках, я добавил код в функцию my_test_user_login() , чтобы сделать свою жизнь проще. Если указан язык lang_code, то после входа user перемещается в указанную языковую версию.

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)

Время тестирования

Для определения времени теста вы можете просто поместить команду времени перед командой Pytest , например:

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

распечатывается в конце терминала:

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

Это результат одного теста с шестью языками, в среднем 2.5 секунды на язык.

Более простой способ увидеть в деталях время выполнения тестов - добавить в командную строку параметр --durations=N. Если N=0, то вы получите время выполнения каждого теста. При N=1 вы получаете время выполнения самого медленного теста. При N=2 вы получаете время выполнения двух самых медленных тестов и т.д. Например, чтобы получить медленную 4 контактную форму теста, команда есть:

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

Результат включает в себя:

========== 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 ==========

Время внутри fixture

Чтобы увидеть, куда все это время уходит, я провел тайм-тест, собирая время на различных этапах в аутентифицированных 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)

Время для входа дольше, чем вы можете ожидать, это вызвано преднамеренной задержкой при входе в систему. Тесты с неавторизованными fixture, client_lang_code_an() в среднем занимают 1.5 секунды, что ожидается без входа в систему.

Сокращение времени тестирования путем изменения области видимости Pytest

Так как область видимости Pytest fixture является 'функцией', мы получаем новый test_client для каждого теста. Я использовал scope='function', чтобы увидеть выполнение тестов для всех языков. Вывод 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

Это очень мило, но также занимает много времени.

Для уменьшения времени выполнения теста я изменяю область видимости fixture на 'сеанс'. В этом случае новый test_client будет создан только для каждого языка. Сначала все тесты выполняются для языка 'de', затем все тесты выполняются для языка 'en' и т.д.. Вывод 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

Таким образом, время тестирования значительно сокращается. Но только если вы выберете много, все тесты, конечно. В целом большинство тестов выполняется очень быстро, и лишь немногие из них отвечают за 80% времени тестирования.

Время тестирования для 14 (contact_form) тестов с использованием scope='function' было 00:06:11. После смены scope на 'сессию' время составило 00:04:56. В этом случае сокращение времени тестирования составило 20%. В другом случае уменьшение составило более 50%.

Дизайн для тестирования

Чем больше данных мы можем извлечь из нашего приложения, тем больше мы можем автоматизировать тестирование. Например, что такое prefix формы логина? Какие сообщения об ошибке при попытке входа в систему, если она уже авторизована? Какое минимальное и максимальное количество символов для пароля? Эти данные присутствуют в приложении, как их получить?

Для ряда классов я добавил класс 'Info', который является родителем данного класса.

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())
        ...

До сих пор не знаю, можно ли это сделать лучше, но это выполнимо.

Автоматическая проверка языка

В вышеприведенных тестах мы не знаем, действительно ли сообщения корректны и переведены для каждого языка. Мы должны вручную проверить тексты, но можем попытаться минимизировать нашу работу.

Я создал функцию check_text() , которая делает три вещи:

  • Хранить текст в словаре
  • Проверьте, нет ли такого же текста в словаре.
  • Хранить текст в языковом лог-файле

Названия этих языковых лог-файлов являются именами тестов. Тест выполняется для разных языков. Если текст уже есть в словаре, то, вероятно, что-то не так, например, текст не переведен. В этом случае я вызываю 'assert' в функции check_text(). Это свидетельствует об ошибке и мы можем расследовать, что не так.

Если все в порядке, то я проверяю языковой лог-файл, чтобы посмотреть на тексты. Поля строк в этом файле разделены на табуляции: 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	Вы уже вошли в систему.

Я не знаю русского и только немного испанского и французского, но думаю, что это выглядит прекрасно.

Подробнее о функции check_text()

Я могу указать text_id, когда хочу проверить более одного текста в тестовой функции. Если мы не укажем это, то будет использовано 'default'.
Я также добавил флаг для текстов, которые мы не хотим проверять (автоматически). Имя теста, вызывающего абонента, извлекается из стека. Функция check_text() первая строка:

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

Мы много чего сделали, но все равно каждый раз после запуска теста нам приходится вручную проверять тексты. Это все еще слишком много работы, мы проверили тексты, переводы, в языковом лог-файле и не хотим делать это снова!

Чтобы этого избежать, я создал директорию 'language_logs_ok' и скопировал лог-файлы языка, которые мы здесь одобрили. Но все равно слишком много работы, мы не хотим копировать файлы вручную!

С помощью дополнительного кода мы можем проверить в конце теста, существует ли этот ok_file. Если он не существует, мы можем попросить одобрить тексты, и в случае ответа Yes мы копируем языковой лог-файл также в директорию 'language_logs_ok'. Если он существует, мы можем сравнить тексты текущего теста с утверждёнными текстами и сообщить об ошибке, если они не совпадают.

Pytest имеет hooks , который можно использовать для добавления функциональности до начала тестов и после их завершения. Здесь я использую только последний:

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

Я добавил код и теперь результат:

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]

Если я одобряю эти тексты, то создается ok_file. Затем при следующем запуске, после результатов теста Pytest , появляется следующее сообщение:

test_an_auth_login_password_too_long: language match with ok_file PASSED

Это снимает не только много стресса, но и много времени!

Другой тест с несколькими текстами

Я использую пакет WTForms . Я могу вытянуть форму в тест, извлечь метки полей формы и проверить, что они находятся на странице. Автоматизируя проверку меток полей формы, мы устраняем пропущенные или неправильные переводы.

Форма авторизации:

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'))

Тест:

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)

И результат после успешного теста:

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] 

Покрытие теста Pytest-cov

Я также установил Pytest-cov , чтобы получить подробную информацию о том, какой объем кода был использован во время теста. Опять же, это очень полезно при тестировании библиотеки, пакета. Вы получаете номера строк кода, который не использовался в тестах. Вы должны получить 100% покрытие!

При проведении функциональных тестов изначально очень сложно достичь этого 100%, так как мы тестируем части приложения. Например, если у нас есть класс проверки входных данных, то при запуске функциональных тестов мы можем пропустить множество методов проверки входных данных.

Иногда я добавляю дополнительный код для повторной проверки правильности значения. Это связано с тем, что я хочу убедиться, что где-то не ошибся, а также иногда я не доверяю стороннему пакету, который я использую. Этот код остается непроверенным. Мне нужно добавить hooks , чтобы сделать этот код тестируемым ... однажды.

Параллельное тестирование

Когда я могу запустить тесты параллельно, партия заканчивается быстрее. Я уже говорил об умышленных задержках, которые вставляются при входе в систему. Какая пустая трата времени на тесты. Но я не могу просто запустить все тесты одновременно. Некоторые тесты модифицируют test_user, другие могут мешать ограничениям на отправку электронной почты и т.д. Я должен тщательно выбирать, какие тесты отстраняются друг от друга. Пакет Pytest-xdist может помочь. Я рассмотрю это в один из этих дней.

Проблемы

При запуске моего первого Pytest fixture под управлением Flask test_client я получил следующую ошибку:

AssertionError: a  localeselector  function is already registered

Flask-Babel с Pytest выдает AssertionError: функция localeselector уже зарегистрирована. С первым параметром ('de') проблем нет, но со следующим ('en', 'es', ...) выводится приведенное выше сообщение об ошибке. Это происходит при создании нового test_client .

Мой обходной путь, который, похоже, отлично работает, это использование глобальной переменной, которая указывает, была ли уже декорирована localeselector . Если нет, то я декорирую его сам.

    # 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

Резюме

Это моя первая реализация в основном функциональных тестов для этого мультиязычного сайта Flask CMS/Blog. На настройку и написание вспомогательных функций ушло достаточно много времени. Но они необходимы для написания компактных тестов. И я уверен, что мне придется писать новые вспомогательные функции для новых категорий тестов.

Я не хочу жестко кодировать тексты и параметры в тесты. Тексты и параметры могут меняться в течение всего срока службы продукта. Я изменил код и добавил классы 'Info', чтобы можно было подтягивать тексты и параметры, которые я хочу тестировать.

Тесты с текстами требуют взаимодействия и должны быть одобрены вручную. После одобрения я могу провести те же самые тесты, и тексты будут автоматически проверяться на соответствие утвержденным текстам. Если тексты изменятся, будет представлена ОШИБКА, и нас снова попросят утвердить эти изменения.

Я также внедрил некоторые тесты 30+ с Selenium. Но сначала я решил сосредоточиться на Pytest + Flask .

Осталось еще много желаемого, но сначала позвольте мне написать еще немного кода и тестов ...

Ссылки / кредиты

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/

Подробнее

Flask Pytest Testing

Оставить комментарий

Комментируйте анонимно или войдите в систему, чтобы прокомментировать.

Комментарии (1)

Оставьте ответ

Ответьте анонимно или войдите в систему, чтобы ответить.

avatar

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