Функциональное тестирование мультиязычного сайта Flask с Pytest
Используя Pytest fixtures и hooks и Flask test_client мы можем запускать тесты и вручную утверждать тексты.
Тестирование - это тяжелая работа. Она полностью отличается от создания или модификации функциональности. Я знаю, я также разработал компьютерное оборудование, интегральные схемы, системы тестирования аппаратуры. Я написал тесты для 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/
Оставить комментарий
Комментируйте анонимно или войдите в систему, чтобы прокомментировать.
Комментарии (1)
Оставьте ответ
Ответьте анонимно или войдите в систему, чтобы ответить.
Thank you for such a thorough setup. I'll be copying things from here for sure!
Недавний
- Скрытие первичных ключей базы данных UUID вашего веб-приложения
- Don't Repeat Yourself (DRY) с Jinja2
- SQLAlchemy, PostgreSQL, максимальное количество строк для user
- Показать значения в динамических фильтрах SQLAlchemy
- Безопасная передача данных с помощью шифрования Public Key и pyNaCl
- rqlite: альтернатива dist с высокой степенью готовности и SQLite
Большинство просмотренных
- Используя Python pyOpenSSL для проверки SSL-сертификатов, загруженных с хоста
- Использование UUID вместо Integer Autoincrement Primary Keys с SQLAlchemy и MariaDb
- Подключение к службе на хосте Docker из контейнера Docker
- Использование PyInstaller и Cython для создания исполняемого файла Python
- SQLAlchemy: Использование Cascade Deletes для удаления связанных объектов
- Flask Удовлетворительный запрос API проверка параметров запроса с помощью схем Маршмэллоу