Первый CI для PHP и Laravel: запускаем тесты в GitHub Actions

Pipeline GitHub Actions автоматически проверяет код и тесты PHP

CI — это автоматический ответ на вопрос: «Сохранило ли изменение ожидаемое поведение проекта?» Чтобы научиться им пользоваться, недостаточно вставить workflow со строкой composer test, если такой команды в проекте нет. Поэтому начнём с маленького PHP-проекта, напишем реальный тест и проведём изменение через красный и зелёный запуск GitHub Actions.

После урока: у вас будут Composer-проект с автозагрузкой, два теста, workflow и воспроизводимая процедура диагностики. Локально нужны PHP 8.4, расширения DOM, XML, mbstring, Composer 2 и Git. Пример использует PHPUnit 12, которому требуется PHP 8.3+. Аккаунт GitHub нужен для удалённого запуска, а тесты можно выполнить и без него.

1. Подготовьте проект и зависимости

Создайте каталог php-ci-lab с подпапками src, tests и .github/workflows. В корне сохраните composer.json:

{
  "name": "devops-lab/release-label",
  "description": "A minimal release label and CI demonstration",
  "license": "MIT",
  "type": "project",
  "require": {"php": "^8.4"},
  "require-dev": {"phpunit/phpunit": "^12.0"},
  "autoload": {"psr-4": {"App\\": "src/"}},
  "scripts": {"test": "phpunit tests"}
}

Выполните composer install. Первый запуск создаст composer.lock; включите его в Git. Последующие установки будут использовать записанные версии. composer update нужен для осознанного обновления зависимостей, а не для каждой CI-проверки. Каталог vendor в репозиторий не добавляйте.

Где разместить проект

Для сайтов, ботов и pet-проектов можно использовать Beget: виртуальный хостинг от 420 ₽/мес., VPS от 330 ₽/мес. и тестовый период.

Перейти на Beget →

2. Напишите код с проверяемым контрактом

Файл src/ReleaseLabel.php:

<?php
declare(strict_types=1);

namespace App;

use InvalidArgumentException;

final readonly class ReleaseLabel
{
    public function __construct(private string $value)
    {
        if (trim($value) === '') {
            throw new InvalidArgumentException('Release label is empty');
        }
    }

    public function toString(): string
    {
        return $this->value;
    }
}

У объекта одна ответственность: представлять непустое обозначение релиза. Он не читает окружение и не делает HTTP-запросов. Это позволяет проверить поведение без базы, сети и запуска веб-сервера.

Файл tests/ReleaseLabelTest.php:

<?php
declare(strict_types=1);

use App\ReleaseLabel;
use PHPUnit\Framework\TestCase;

require_once __DIR__ . '/../vendor/autoload.php';

final class ReleaseLabelTest extends TestCase
{
    public function testPreservesValidLabel(): void
    {
        self::assertSame('v0.1.0', (new ReleaseLabel('v0.1.0'))->toString());
    }

    public function testRejectsEmptyLabel(): void
    {
        $this->expectException(InvalidArgumentException::class);
        new ReleaseLabel('   ');
    }
}

Запустите composer test. Ожидайте успешное завершение и две пройденные проверки. Формат строки summary зависит от версии PHPUnit. Важно, что число тестов равно двум, а код завершения — ноль. На Linux его можно посмотреть отдельной командой echo $? сразу после запуска.

3. Добавьте полноценный workflow

Файл .github/workflows/tests.yml:

name: PHP tests
on:
  pull_request:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read

jobs:
  tests:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v7
        with:
          persist-credentials: false
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.4'
          extensions: dom, xml, mbstring, xmlwriter
          coverage: none
          tools: composer:v2
      - name: Validate dependency definition
        run: composer validate --strict
      - name: Install locked dependencies
        run: composer install --no-interaction --prefer-dist --no-progress
      - name: Check syntax
        run: php -l src/ReleaseLabel.php
      - name: Run behavior tests
        run: composer test

Сначала загружаются исходники, затем готовятся PHP и Composer, устанавливаются зависимости и запускаются проверки. В permissions нет права записи: тестовому заданию оно не требуется. persist-credentials: false не оставляет токен checkout в Git-конфигурации для последующих команд. Major-теги Actions удобны для учебного примера; для чувствительного проекта зафиксируйте действия по проверенным полным commit SHA.

timeout-minutes ограничивает продолжительность задания. Он не исправляет зависание, но помогает не тратить ресурсы бесконечно. workflow_dispatch позволяет вручную повторить workflow на выбранной ветке после добавления файла в основную ветку.

4. Передайте проект в GitHub и докажите, что CI ловит ошибку

Добавьте vendor/ в .gitignore. Зафиксируйте исходники, тесты, workflow, composer.json и composer.lock. Отправьте их в свой репозиторий. Откройте вкладку Actions и проверьте первый запуск.

Создайте ветку experiment/broken-label. Измените toString(), чтобы метод возвращал 'broken', и отправьте ветку в pull request. Теперь тест сохранения версии обязан упасть: ожидалось v0.1.0, получено broken. Верните правильный код новым коммитом и дождитесь зелёного результата.

Это важнее значка CI в README: вы проверили, что конкретная регрессия обнаруживается. В настройках защиты main назначьте этот check обязательным, если выбранный план и тип репозитория поддерживают правило. Сам workflow без правила слияния не запрещает человеку нажать merge.

5. Адаптируйте подготовку к Laravel

В существующем Laravel-проекте не заменяйте composer.json учебным. Используйте его lock-файл и штатные тесты. Для проекта, который поддерживает SQLite и выполняет тестовые миграции через RefreshDatabase, добавьте расширение pdo_sqlite и после установки зависимостей выполните:

      - name: Prepare disposable Laravel environment
        env:
          APP_ENV: testing
          DB_CONNECTION: sqlite
          DB_DATABASE: ':memory:'
          SESSION_DRIVER: array
          CACHE_STORE: array
          QUEUE_CONNECTION: sync
        run: |
          cp .env.example .env.testing
          php artisan key:generate --env=testing
          php artisan test --env=testing

Этот шаг заменяет composer test в Laravel workflow либо вызывается отдельным тестовым заданием. Проверьте phpunit.xml: принудительные значения из него могут изменить ожидаемое окружение. Имена параметров кэша зависят от поколения приложения; используйте настройки своей версии Laravel.

Не запускайте миграции в отдельном PHP-процессе для SQLite :memory: в надежде сохранить таблицы до следующей команды: память принадлежит соединению. Тестовый framework должен подготовить схему в процессе тестов. Если проект требует MySQL-специфичные возможности, добавьте одноразовый MySQL service; подмена его SQLite не доказывает совместимость SQL.

6. Диагностируйте первый неуспешный шаг

  • Class App\ReleaseLabel not found: сравните регистр имени файла, namespace и PSR-4-префикс. Выполните composer dump-autoload локально; Linux учитывает регистр путей.
  • Missing ext-dom или ext-mbstring: добавьте нужное расширение, а не отключайте platform requirements.
  • Lock file is not up to date: осознанно обновите lock локально и закоммитьте его вместе с composer.json.
  • Тест зелёный при испорченном коде: проверьте путь tests, число найденных тестов и отсутствие continue-on-error или || true.

Самостоятельная работа: добавьте запрет перевода строки в метке релиза, сначала напишите падающий тест, затем исправьте класс. Успех — красный запуск до исправления и зелёный после него. Не добавляйте тестовому workflow доступ к серверу: поставка приложения — отдельная задача с отдельными правами.

Официальные материалы: требования PHPUnit 12, синтаксис workflow, тестирование Laravel.

Хотите системно изучить программирование?

Подборка онлайн-курсов для разработчиков — Python, Java, веб и DevOps.

Смотреть курсы →

Бесплатные инструменты

Калькулятор подсетей · SSH-конфиг · Nginx · Docker Compose

Нужно настроить сервер, сайт или автоматизацию?

Разберём задачу и предложим понятный состав работ, срок и стоимость.

Узнать состав и стоимость → 8 918 532 81 08

Статья может содержать партнёрские ссылки. Это не влияет на стоимость для вас, но помогает развивать блог.