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

В этой статье я расскажу о том, как можно автоматизировать запуск тестов вашего хобби-проекта на Python. Автоматизированное тестирование может быть реализовано несколькими способами, но в этой статье я ограничусь явным описанием одного и концептуальным описанием других. Тот, на котором я сосредоточусь, является самым простым в реализации. Как и в других статьях этой серии, я в хронологическом порядке подробно расскажу о шагах, которые я предпринял для достижения этого в своем хобби-проекте. Это должно убедить вас в том, что разработка произвольного сложного приложения может и должна выполняться поэтапно. Один шаг за раз.

На этом этапе мой проект вырос до стадии с несколькими файлами исходного кода в моем каталоге пакетов factpy и рядом модульных тестов в нескольких файлах в каталоге tests. В процессе разработки я постоянно добавляю функциональность, пишу соответствующие тесты, запускаю эти тесты с помощью pytest и отправляю свой код в удаленный репозиторий на Bitbucket. Это хорошо отлаженный процесс. Но что, если я забуду запустить свои тесты и отправлю их прямо в удаленный репозиторий? Может и нет проблем. С акцентом на могут. Если бы тест провалился, я бы не обнаружил эту проблему. Конечно, я замечу это в следующий раз, когда буду запускать все тесты, но в это время я, вероятно, буду работать над несвязанной частью кода, а это означает, что мне придется переключить свое внимание. Вы можете подумать: «Пффф, это не проблема!» Но я гарантирую, что после нескольких раз вы довольно быстро разочаруетесь.

Но что, если я забуду запустить свои тесты и отправлю их прямо в удаленный репозиторий?

Итак, приступим! Самый простой способ автоматизировать ваши тесты — запускать их каждый раз, когда вы отправляете их в удаленный репозиторий. К счастью, все известные службы хостинга репозиториев с контролем версий имеют для этого встроенную функциональность. У Bitbucket и Gitlab есть Pipelines, у Github есть Github Actions, а у AWS есть CodeBuild. Эти инструменты являются частью непрерывной интеграции и непрерывной доставки/развертывания. процесс, обычно сокращенно CI/CD. Эти конвейеры CI / CD часто состоят из нескольких этапов, которые будут выполняться, таких как создание кода, код тестирования и код развертывания. На практике они, скорее всего, будут комбинацией контейнеров Docker. Но просто подумайте о конвейере как об отдельной машине, вроде вашей собственной, на которой вы можете выполнять список команд. Выполнение этого списка или запуск конвейера можно выполнить вручную или с помощью триггера, такого как отправка в репозиторий. Поскольку мой репозиторий размещен на Bitbucket, я просто упомяну их вариант конвейера.

Bitbucket упрощает создание вашего первого конвейера, предлагая множество готовых шаблонов. Он даже предлагает использовать шаблон Сборка и тестирование Python для моего проекта. У них также есть руководство и страница документации для конвейеров Python.

Исходя из такого шаблона, немного изменив его и удалив части, которые мне пока не нужны, спецификации пайплайна для моего проекта сводятся к этому файлу bitbucket-pipelines.yml в корневом каталоге проекта:

# This is a sample build configuration for Python.
# Check our guides at https://confluence.atlassian.com/x/x4UWN for more examples.
# Only use spaces to indent your .yml configuration.
# -----
# You can specify a custom docker image from Docker Hub as your build environment.
image: python:3.8
pipelines:
  default:
    - step:
        script: 
          - pip install -r pip-requirements.txt
          - python -m pytest

Он использует Официальный образ Python 3.8 Docker в качестве базового образа, в котором предустановлен Python с общими библиотеками. Определен только конвейер default, который применяется ко всем ветвям, которые не соответствуют другому определению конвейера в файле. Конвейер по умолчанию запускается при каждой отправке в репозиторий, если не определен конвейер для конкретной ветки. Затем определяется step, где каждый шаг загружает новый контейнер Docker, который включает клон текущего репозитория. На этом шаге script содержит список команд, которые выполняются по порядку. Этот конкретный конвейер будет выполнять две команды: pip install и pytest. Я вернусь к тому, почему я использую pip вместо conda, как на моей машине разработки. Подробнее о том, как создать такой bitbucket-pipelines.yml файл, можно узнать в документации.

Использование conda на сервере CI или образе Docker не является простым, поскольку вы не можете эмулировать активацию сред conda, но вам нужна собственная инфраструктура активации Conda. Тем не менее, вы можете использовать conda, но вам потребуется более сложная настройка, и это выходит за рамки этой статьи. Поскольку pip предустановлен в образе Python, им удобнее пользоваться. Чтобы установить пакеты с помощью pip, мне нужно перечислить пакеты из моей среды conda в формате, удобном для pip. Несмотря на то, что вам следует избегать смешивания pip и conda, использование pip freeze > pip-requirements.txt (внутри вашей среды) правильно вернет установленные пакеты, независимо от того, установлены они с conda или pip. Обратите внимание, что я не даю 100-процентной гарантии, что это всегда будет работать. Мой файл pip-requirements.txt выглядит так:

attrs==19.3.0
autopep8==1.4.4
certifi==2020.4.5.1
more-itertools==8.2.0
mypy==0.770
mypy-extensions==0.4.3
packaging==20.3
pluggy==0.13.1
psutil==5.7.0
py==1.8.1
pycodestyle==2.5.0
pyparsing==2.4.7
pytest==5.4.2
six==1.14.0
typed-ast==1.4.1
typing-extensions==3.7.4.1
wcwidth==0.1.9

Для запуска модульных тестов конвейер выполняет команду python -m pytest. Поскольку мне не нужно часто вводить эту команду, мне не нужно настраивать путь среды CI Python, чтобы он мог запускать тесты с помощью более короткой команды pytest (как я сделал в предыдущей статье).

Вот и все! С этого момента каждое нажатие на удаленный репозиторий будет запускать конвейер, который будет запускать все модульные тесты. Результаты этого запуска можно проверить в Bitbucket. Результатом будет либо неудачный, либо успешный запуск.

Автоматический запуск теста после отправки в удаленный репозиторий — это хорошо, но это все же позволяет тестам терпеть неудачу после отправки, если вы не проверили все локально. Чтобы отловить неудачные тесты перед отправкой, вы можете создать так называемые git hooks, которые представляют собой скрипты, которые запускаются автоматически каждый раз, когда в репозитории Git происходит определенное событие. Вы можете найти образцы таких скриптов в каталоге .git/hooks вашего репозитория. В частности, хук pre-commit можно использовать для автоматического запуска модульного теста перед фактической фиксацией в локальном репозитории. Если тесты не пройдены, фиксация будет прервана. Это позволяет быстрее выявлять ошибки и лучше применять движение shift-left.

Тестирование со сдвигом влево — это подход к тестированию программного обеспечения и тестированию системы, при котором тестирование выполняется на более ранних этапах жизненного цикла.

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