Разделы портала

Онлайн-тренинги

.
Легкое веб-тестирование с Python, Pytest и Selenium WebDriver, часть 6: как читать файлы конфигураций в тестах Python Selenium
26.08.2020 00:00

Автор: Энди Найт (Andy Knight)
Оригинал статьи
Перевод: Ольга Алифанова

Какой браузер?

Наш тест поиска DuckDuckGo отлично работает… в Chrome. Давайте еще раз посмотрим на фикстуру браузера:

  1. @pytest.fixture
  2. def browser():
  3. driver = Chrome()
  4. driver.implicitly_wait(10)
  5. yield driver
  6. driver.quit()

И тип драйвера, и время ожидания жестко зашиты в код. Это хорошо для подтверждения работоспособности, но готовым к проду тестам нужна возможность конфигурировать на лету. Web UI-тесты нужно мочь прогонять в любом браузере. Нужно иметь возможность менять значения таймаута по умолчанию для случаев, когда какие-либо окружения медленнее прочих. Другие закрытые данные вроде логинов и паролей тоже никогда не должны отображаться в исходном коде. Как управлять подобными тест-данными?

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

Источники ввода

Считывание ввода в системы тест-автоматизации возможно несколькими способами:

  • Аргументы командной строки
  • Переменные окружения
  • Системные свойства
  • Конфиг-файлы
  • Сервисные вызовы api.

К сожалению, большинство ключевых тест-фреймворков не поддерживают кастомные аргументы командной строки. Переменными окружения и системными свойствами зачастую сложно управлять, и они потенциально опасны в обращении. Службы API – отличный способ передавать ввод,

Unfortunately, most core test frameworks don’t support custom command line arguments. Environmental variables and system properties can be difficult to manage and potentially dangerous to handle. Service APIs are a great way to consume inputs, especially for getting secrets (like passwords) from a key management service like AWS KMS or Azure Key Vault. However, paying for such a service may not be permissible, and writing your own may not be sensible. For lean cases, config files may be the best option.

Файл конфигурации – это просто файл с конфигурационными данными. Автоматизация может читать его при запуске тестов и использовать входные данные для контроля этих тестов. К примеру, конфиг-файл может описывать тип браузера для использования фикстурой браузера в нашем учебном проекте. Хорошая практика – составлять конфиг-файл в стандартном формате вроде JSON, YAML или INI. Он должен также быть плоским для возможности диффов.

Наш конфиг-файл

Давайте создадим конфиг-файл для нашего учебного проекта. Мы будем использовать JSON, потому что это простой, популярный и иерархичный формат. К тому же модуль json – часть стандартной библиотеки Python, которая может легко конвертировать файлы JSON в словари. Создайте новый файл tests/config.json и добавьте в него код:

  1. {
  2. "browser": "chrome",
  3. "wait_time": 10
  4. }

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

Чтение конфиг-файлов с Pytest

Наилучший способ читать конфиг-файлы в pytest – это фикстуры. Они могут читать конфиги до старта тестов, а затем вставлять значения в тесты или другие фикстуры. Добавьте фикстуру в файл tests/test_web.py:

  1. import json
  2. @pytest.fixture(scope='session')
  3. def config():
  4. with open('tests/config.json') as config_file:
  5. data = json.load(config_file)
  6. return data

Конфигурационная фикстура читает и парсит файл tests/config.json в словарь данных, используя модуль json. Жестко закодированные пути к файлам – распространенная практика. На самом деле множество инструментов и систем автоматизации будет искать файлы в множестве локаций или по паттернам имен. Область действия фикстуры установлена как "session" – фикстура будет запускаться только один раз для каждой тест-сессии. Нет необходимости заново считывать тот же самый конфиг для каждого теста – это неэффективно.

Оба значения конфигурационных данных нужны при инициализации WebDriver. Обновите браузерную фикстуру:

  1. @pytest.fixture
  2. def browser(config):
  3. if config['browser'] == 'chrome':
  4. driver = Chrome()
  5. else:
  6. raise Exception(f'"{config["browser"]}" is not a supported browser')
  7. driver.implicitly_wait(config['wait_time'])
  8. yield driver
  9. driver.quit()

Теперь у браузерной фикстуры есть зависимость от фикстуры конфига. Несмотря на то, что конфиг будет запускаться только один раз за сессию, браузер будет вызываться перед каждым тестом. У браузера теперь есть цепочка if-else для определения, какой тип WebDriver использовать. Сейчас поддерживается только Chrome, но скоро мы добавим другие типы. Ошибка будет выдаваться, если выбор браузера не распознан. Неявное время ожидание теперь тоже использует значение из конфигурационных данных.

Так как браузерная фикстура все еще возвращает копию WebDriver, использующие ее тесты не нуждаются в переработке! Давайте запустим тесты, чтобы убедиться, что конфиг-файл работает:

  1. $ pipenv run python -m pytest tests/test_web.py
  2. ============================= test session starts ==============================
  3. platform darwin -- Python 3.7.3, pytest-4.5.0, py-1.8.0, pluggy-0.12.0
  4. rootdir: /Users/andylpk247/Programming/automation-panda/python-webui-testing
  5. collected 1 item
  6. tests/test_web.py . [100%]
  7. =========================== 1 passed in 5.00 seconds ===========================

Добавление новых браузеров

Теперь, когда у проекта есть конфиг-файл, его можно использовать для изменения браузера. Давайте запустим тест, используя не Google Chrome, а Mozilla Firefox. Скачайте и установите последнюю версию Firefox, а затем – последнюю версию geckodriver (драйвер для Firefox). Убедитесь, что geckodriver добавлен в системные пути.

Обновите код браузерной фикстуры для поддержки Firefox:

  1. from selenium.webdriver import Chrome, Firefox
  2. @pytest.fixture
  3. def browser(config):
  4. if config['browser'] == 'chrome':
  5. driver = Chrome()
  6. elif config['browser'] == 'firefox':
  7. driver = Firefox()
  8. else:
  9. raise Exception(f'"{config["browser"]}" is not a supported browser')
  10. driver.implicitly_wait(config['wait_time'])
  11. yield driver
  12. driver.quit()

Затем обновите конфиг-файл опцией "firefox":

  1. {
  2. "browser": "firefox",
  3. "wait_time": 10
  4. }

Перезапустите тест. ВЫ увидите, как откроется Firefox, а не Chrome!

Валидация

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

Добавьте новую фикстуру для валидации выбора браузера:

  1. @pytest.fixture(scope='session')
  2. def config_browser(config):
  3. if 'browser' not in config:
  4. raise Exception('The config file does not contain "browser"')
  5. elif config['browser'] not in ['chrome', 'firefox']:
  6. raise Exception(f'"{config["browser"]}" is not a supported browser')
  7. return config['browser']

Фикстура config_browser зависит от фикстуры config. Как и у config, ее область действия – сессия. Она выдаст ошибку, если в конфиг-файле нет ключа "browser", или если выбранный браузер не поддерживается. И, наконец, она возвращает выбранный браузер, и тесты и прочие фикстуры могут удобно воспользоваться этим значением.

Теперь добавьте фикстуру для валидации времени ожидания:

  1. @pytest.fixture(scope='session')
  2. def config_wait_time(config):
  3. return config['wait_time'] if 'wait_time' in config else 10

Если в конфиг-файле указано время ожидания, фикстура config_wait_time вернет его. В прочих случаях она вернет значение по умолчанию – 10 секунд.

Обновите браузерную фикстуру еще раз, чтобы использовать новые фикстуры валидации:

  1. @pytest.fixture
  2. def browser(config_browser, config_wait_time):
  3. if config_browser == 'chrome':
  4. driver = Chrome()
  5. elif config_browser == 'firefox':
  6. driver = Firefox()
  7. else:
  8. raise Exception(f'"{config_browser}" is not a supported browser')
  9. driver.implicitly_wait(config_wait_time)
  10. yield driver
  11. driver.quit()

Создание отдельных фикстурных функций для каждого значения конфигурации делает их простыми, внятными, и сфокусированными. Они также позволяют вызывающим функциям объявлять только нужные им значения.

Запустите тест, чтобы убедиться, что в ситуации "счастливого пути" все работает.

  1. $ pipenv run python -m pytest tests/test_web.py
  2. ============================= test session starts ==============================
  3. platform darwin -- Python 3.7.3, pytest-4.5.0, py-1.8.0, pluggy-0.12.0
  4. rootdir: /Users/andylpk247/Programming/automation-panda/python-webui-testing
  5. collected 1 item
  6. tests/test_web.py . [100%]
  7. =========================== 1 passed in 4.58 seconds ===========================

Отлично! Однако для того, чтобы действительно проверить валидацию, надо пойти окольными путями. Давайте поменяем значение "browser" в файле tests/config.json на неподдерживаемый браузер "safari". Перезапустив тест, мы должны увидеть говорящее сообщение об ошибке:

  1. $ pipenv run python -m pytest tests/test_web.py
  2. ============================= test session starts ==============================
  3. platform darwin -- Python 3.7.3, pytest-4.5.0, py-1.8.0, pluggy-0.12.0
  4. rootdir: /Users/andylpk247/Programming/automation-panda/python-webui-testing
  5. collected 1 item
  6. tests/test_web.py E [100%]
  7. ==================================== ERRORS ====================================
  8. ________________ ERROR at setup of test_basic_duckduckgo_search ________________
  9. config = {'browser': 'safari', 'wait_time': 10}
  10. @pytest.fixture(scope='session')
  11. def config_browser(config):
  12. # Валидация и возвращение выбранного браузера из конфигурационных данных
  13. if 'browser' not in config:
  14. raise Exception('The config file does not contain "browser"')
  15. elif config['browser'] not in SUPPORTED_BROWSERS:
  16. > raise Exception(f'"{config["browser"]}" is not a supported browser')
  17. E Exception: "safari" is not a supported browser
  18. tests/conftest.py:30: Exception
  19. =========================== 1 error in 0.09 seconds ============================

Отлично! При падении четко сообщено о проблеме. А что будет, если мы уберем браузер из конфиг-файла?

  1. $ pipenv run python -m pytest tests/test_web.py
  2. ============================= test session starts ==============================
  3. platform darwin -- Python 3.7.3, pytest-4.5.0, py-1.8.0, pluggy-0.12.0
  4. rootdir: /Users/andylpk247/Programming/automation-panda/python-webui-testing
  5. collected 1 item
  6. tests/test_web.py E [100%]
  7. ==================================== ERRORS ====================================
  8. ________________ ERROR at setup of test_basic_duckduckgo_search ________________
  9. config = {'wait_time': 10}
  10. @pytest.fixture(scope='session')
  11. def config_browser(config):
  12. # Валидация и возвращение выбранного браузера из конфигурационных данных
  13. if 'browser' not in config:
  14. > raise Exception('The config file does not contain "browser"')
  15. E Exception: The config file does not contain "browser"
  16. tests/conftest.py:28: Exception
  17. =========================== 1 error in 0.10 seconds ============================

Отлично! Еще одно полезное сообщение о падении. И, наконец, для финального теста нужно добавить назад валидный выбор браузера, и удалить время ожидания:

  1. $ pipenv run python -m pytest tests/test_web.py
  2. ============================= test session starts ==============================
  3. platform darwin -- Python 3.7.3, pytest-4.5.0, py-1.8.0, pluggy-0.12.0
  4. rootdir: /Users/andylpk247/Programming/automation-panda/python-webui-testing
  5. collected 1 item
  6. tests/test_web.py . [100%]
  7. =========================== 1 passed in 4.64 seconds ===========================

Тест должен пройти, потому что время ожидания опционально. С нашими изменениями все в порядке! Помните, иногда нужно тестировать свои тесты.

Финальный код

Еще одна пара мелочей поможет нам очистить наш тестовый код. Для начала давайте переместим наши веб-фикстуры в файл conftest.py, чтобы они могли использоваться всеми тестами, а не только тестами в tests/test_web.py. Затем сделаем некоторые литеральные значения модульными переменными.

Создайте новый файл tests/conftest.py со следующим кодом:

  1. import json
  2. import pytest
  3. from selenium.webdriver import Chrome, Firefox
  4. CONFIG_PATH = 'tests/config.json'
  5. DEFAULT_WAIT_TIME = 10
  6. SUPPORTED_BROWSERS = ['chrome', 'firefox']
  7. @pytest.fixture(scope='session')
  8. def config():
  9. # Чтение конфиг-файла JSON, возвращение в виде словаря
  10. with open(CONFIG_PATH) as config_file:
  11. data = json.load(config_file)
  12. return data
  13. @pytest.fixture(scope='session')
  14. def config_browser(config):
  1. # Валидация и возвращение выбранного браузера из конфигурационных данных
  1. if 'browser' not in config:
  2. raise Exception('The config file does not contain "browser"')
  3. elif config['browser'] not in SUPPORTED_BROWSERS:
  4. raise Exception(f'"{config["browser"]}" is not a supported browser')
  5. return config['browser']
  6. @pytest.fixture(scope='session')
  7. def config_wait_time(config):
  8. # Валидация и возвращение времени ожидания из конфиг-файла
  9. return config['wait_time'] if 'wait_time' in config else DEFAULT_WAIT_TIME
  10. @pytest.fixture
  11. def browser(config_browser, config_wait_time):
  12. # Initialize WebDriver
  13. if config_browser == 'chrome':
  14. driver = Chrome()
  15. elif config_browser == 'firefox':
  16. driver = Firefox()
  17. else:
  18. raise Exception(f'"{config_browser}" is not a supported browser')
  19. # Неявное ожидание готовности элементов до попытки взаимодействия
  20. driver.implicitly_wait(config_wait_time)
  21. # Возвращение объекта драйвера в конце настройки
  22. yield driver
  23. # Для очистки покиньте драйвер
  24. driver.quit()

Полное содержание tests/test_web.py должно стать проще и чище:

  1. import pytest
  2. from pages.result import DuckDuckGoResultPage
  3. from pages.search import DuckDuckGoSearchPage
  4. def test_basic_duckduckgo_search(browser):
  5. # Set up test case data
  6. PHRASE = 'panda'
  7. # Найти фразу
  8. search_page = DuckDuckGoSearchPage(browser)
  9. search_page.load()
  10. search_page.search(PHRASE)
  11. # Убедиться в появлении результатов
  12. result_page = DuckDuckGoResultPage(browser)
  13. assert result_page.link_div_count() > 0
  14. assert result_page.phrase_result_count(PHRASE) > 0
  15. assert result_page.search_input_value() == PHRASE

Теперь это в духе Python!

Что дальше?

Код нашего учебного проекта готов, и его можно использовать как основу для новых тестов. Полный тестовый проект размещен на GitHub. Но только потому, что мы завершили программирование, не значит, что обучение закончилось. В последнем разделе мы расскажем, как вывести вашу Web UI Python-автоматизацию на новый уровень!

Обсудить в форуме