Первоначально опубликовано на мой блог .
Сегодня на работе я написал сценарий CI, чтобы проверить приложение React, и он оказался немного сложнее, чем ожидалось.
Давайте попробуем воспроизвести интересные проблемы, которые у меня были, и как я их решил.
Вот что вам понадобится, если вы хотите попытаться воспроизвести то, что я сделал.
- Node.js, пряжа
- Python3 и
пипенв
-
Chromedriver
бинарный.
Начнем с создания простого приложения React:
$ yarn global add create-react-app $ create-react-app hello $ cd hello $ yarn
Теперь у нас есть красивое приложение React, работающее в нашем любимом браузере.
Давайте отредактируем App.js
Файл для отображения Привет, мир
вместо:
import React, { Component } from 'react'; class App extends Component { render() { return (Hello, world!
); } } export default App;
Давайте использовать Pipenv
Чтобы создать VirtualEnv с тем, что нам нужно:
$ pipenv install pytest $ pipenv install selenium $ pipenv shell
Теперь давайте добавим немного сквозного, используя Selenium и Pytest 1 Анкет
# in test_hello.py import selenium.webdriver def test_home(): driver = selenium.webdriver.Chrome() driver.get("http://127.0.0.1:3000") assert "Hello, world!" in driver.page_source
Теперь мы можем запустить тесты с Pytest, как обычно:
$ pytest collected 1 item test_hello.py . [100%] 1 passed in 4.77 seconds
Хорошо, это работает!
Теперь давайте представим, что у вас есть команда людей, работающих над приложением, и вы хотели бы, чтобы эти тесты работали в любое время, когда кто -то создает запрос на объединение в этом репо.
Это известно как Непрерывная интеграция (CI для краткости) И, поверьте мне в этом, это работает намного лучше, чем сказать вашим товарищам по команде, чтобы они не могли запустить тесты, прежде чем отправлять изменения для обзора!
Мы используем Gitlab
на работе и являются большим поклонником его функций CI.
Если вы вообще не знаете Gitlab CI, вот как это работает:
- Вы устанавливаете и настраиваете
Gitlab-Runner
Программа на некоторых машинах (называемые бегуны ) - Тогда вы пишете
.gitlab-ci.yml
Файл, который содержит описание работы.
На моей работе мы предпочитаем сохранить .gitlab-ci.yml
просто, и держите код сценариев CI отдельно, как это:
(Обратите внимание, как мы используем python3 -m Pipenv
вместо просто пипенв
. Это чтобы убедиться, что Pipenv
работает с ожидаемой версией Python)
# in .gitlab-ci.yml stages: - check check: stage: check script: - python3 -m pipenv install - python3 -m pipenv run python ci.py
# in ci.py def main(): # Call yarn here if __name__ == " __main__": main()
Мы делаем это, потому что это позволяет легко воспроизводить сбои сборки, найденные во время CI на местном уровне. Любой разработчик в команде может работать Python ci/ci.py
На их машине напрямую вместо того, чтобы пытаться скопировать/вставить код из файла YAML.
Прямо сейчас в тестах селена используется полноценная хромированная хромированность для запуска тестов. Это приятно для разработчиков, но не очень приятно на бегуне Gitlab.
Вместо этого было бы намного лучше, чтобы те, кто бежал в безголовом хром, то есть без какого -либо графического интерфейса.
Давайте исправим это, добавив -Без головой
вариант:
# in conftest.py import pytest def pytest_addoption(parser): parser.addoption("--headless", action="store_true") @pytest.fixture def headless(request): return request.config.getoption("--headless")
# in test_hello.py from selenium.webdriver.chrome.options import Options as ChromeOptions def test_home(headless): options = ChromeOptions() options.headless = headless driver = selenium.webdriver.Chrome(chrome_options=options) ...
Теперь, если мы запустим pytest
с -Без головой
вариант, без головы
параметр test_home
Функция будет установлена на Истинный
от Pytest. Вот как pytest светильники Работа.
В любом случае, теперь мы можем проверить, что это работает, работая:
$ pytest --headless
Итак, теперь мы сталкиваемся с новой задачей: нам нужно запустить пряжа старт
до бег pytest
и убейте сценарий React, когда тесты селена завершены.
Хороший способ сделать это в Python – использовать с
заявление, так что давайте сделаем это:
class BackgroundProcess: """ Run `yarn start` in the background. Ensure the yarn process is killed when exiting the `with` block """ def __init__ (self): self.process = None def __enter__ (self): self.process = subprocess.Popen(["yarn", "start"]) def __exit__ (self, type, value, tb): self.process.terminate() def main(): with BackgroundProcess("yarn", "start"): subprocess.run(["pytest", "--headless"], check=True) if __name__ == " __main__": main()
__enter__
Метод будет вызван прямо перед содержимым с
Блок, так что раньше pytest
начинается. Тогда __exit__
Метод будет вызван после pytest
сделано, Даже если произошло исключение , передавая данные об исключении в качестве аргументов для __exit__ ()
метод Поскольку мы не хотим делать с чем-то, кроме повторного, если это произойдет, мы просто игнорируем их.
В любом случае, это гораздо более читаемо, чем использование Попробуйте/кроме/наконец
ты не думаешь?
Нам все еще нужно крошечное исправление: по умолчанию, пряжа старт
Откроет новую вкладку в нашем браузере. Это было здорово, когда мы работали над кодом JavaScript, но здесь мы работаем над сценарием CI, поэтому мы бы предпочли отключить такое поведение.
К счастью, все, что нам нужно сделать, это установить Браузер
переменная среды к Нет
:
class BackgroundProcess: ... def __enter__ (self): env = os.environ.copy() env["BROWSER"] = "NONE" self.process = subprocess.Popen(self.cmd, env=env)
Примечание: вы можете задаться вопросом, почему мы не просто установили Браузер
переменная среды непосредственно в .gitlab-ci.yml
файл. Это сработало бы, но здесь мы создаем специальное копия текущих переменных среды, и мы устанавливаем Браузер
переменная среды Просто для пряжа
процесс . Почему?
Что ж, если вы думаете о переменных окружающей среды как о неприятных глобальных переменных (и вы должны: среда процесса – это просто большое измененное общее состояние), имеет смысл ограничить их объем таким образом.
В любом случае, вернемся к основной теме:
Примечание. Остальная часть статьи предполагает, что вы используете Linux. Вещи могут работать немного по -другому (или вообще не) в других операционных системах.
Посмотрим, работает ли сценарий CI.
$ python ci.py yarn run v1.7.0 $ react-scripts start Starting the development server... ... 1 passed in 4.77 seconds
Давайте запустим его во второй раз, чтобы проверить, что пряжа
процесс был действительно убит:
$ python ci.py ? Something is already running on port 3000. Probably: hello (pid 16508) Would you like to run the app on another port instead? (Y/n)
Э-э-э.
Давайте запустим pgrep
Чтобы проверить, что пряжа
процесс мертв:
$ pgrep yarn [err 1]
Процесс пряжи это мертвых. Что дает ?
Если мы посмотрим на .Terminate ()
Реализация, вот что мы находим:
# in /usr/lib/python3.6/subprocess.py class Popen: def send_signal(self, sig): """Send a signal to the process.""" # Skip signalling a process that we know has already died. if self.returncode is None: os.kill(self.pid, sig) def terminate(self): """Terminate the process with SIGTERM """ self.send_signal(signal.SIGTERM)
Итак, Terminate ()
Просто отправляет Sigterm
Сигнал с использованием идентификатора процесса ( pid
). Ошибки там нет.
Правда в том, что мы только что создали сироту (мы монстры!)
Когда мы бежали пряжа старт
, пряжа
Процесс посмотрел раздел под названием старт
в Package.json
и нашел что -то вроде этого:
{ ... "scripts": { "start": "react-scripts start", ... } }
Затем он создал Ребенок процесс, а именно React-Scripts Start
, с другой пид Анкет
Поэтому, когда мы убили родительский процесс, Узел
Процесс стал сиротой, так как ее родитель был мертв (плохой небольшой процесс).
По крайней мере, на Linux все процессы сирот автоматически переоцениваются в первый в истории процесс, который был создан с момента загрузки машины. ( Systemd
на моей машине). Этот процесс всегда имеет PID, равный 1 и часто называют init
Анкет
Мы можем проверить это, запустив пиро
:
$ pstree systemd─┬ <- PID 1 ... ├─node── <- our poor orphan ... ├─plasmashell─┬ ├─konsole─┬─zsh─ <- our shell
Итак, как мы убедитесь, что Узел
Детский процесс тоже убивает?
Есть несколько причудливых способов решить такие проблемы (например, мы могли бы использовать cgroups
, но мы можем сделать это только со Stdlib Python.
Оказывается, мы можем использовать start_new_session
аргумент в подпроцесс. Popen ()
вызов. Это создаст сессия и прикрепить пряжа
процесс (и всех его детей) к нему.
Тогда мы можем отправить Sigterm
сигнал на пид родителя, и все процессы в сеансе получат его:
import os import signal def __enter__ (self): ... self.process = subprocess.Popen(self.cmd, start_new_session=True) def __exit__ (self): os.killpg(self.process.pid, signal.SIGTERM)
Теперь, если мы перезапустим наш сценарий, мы увидим, что ни один пряжа
или Узел
оставаться живым, когда сценарий CI завершается:
$ python ci.py $ pgrep yarn [err 1] $ pgrep node [err 1]
Это все на сегодня. Ваше здоровье!
Спасибо, что прочитали это далеко:)
Я хотел бы услышать, что вы скажете, поэтому, пожалуйста, оставьте комментарий ниже или прочитайте Страница обратной связи Для большего количества способов связаться со мной.
Это не первый раз, когда я использовал эти инструменты для написания сквозных тестов для веб-приложения. Смотрите Портирование в pytest например. ↩
Оригинал: “https://dev.to/dmerejkowsky/adventures-in-ci-land-44oo”