Тестирование WordPress: PHPUnit, Playwright и автоматизация CI/CD

Обновление ядра WordPress, установка нового плагина или правка в functions.php часто приводят к скрытым поломкам: перестают работать шорткоды, отваливается форма обратной связи или ломается верстка на мобильных устройствах. Ручная проверка всех сценариев перед каждым деплоем занимает часы и не гарантирует отсутствия ошибок.

Решение — двухуровневое автоматическое тестирование. PHPUnit проверяет серверную логику (PHP-функции, хуки, REST API), а Playwright эмулирует действия пользователя в браузере (E2E-тесты), проверяя, что фронтенд корректно взаимодействует с бэкендом. В связке с CI/CD пайплайном это создает надежный барьер на пути багов в продакшн.

Уровень 1: Unit и Integration тесты через PHPUnit

PHPUnit позволяет тестировать изолированные куски кода. В экосистеме WordPress для этого используется официальная утилита @wordpress/env, которая разворачивает локальный Docker-контейнер с чистой базой данных для каждого прогона тестов.

Шаг 1: Установка окружения

В корне темы или плагина инициализируем Node.js и устанавливаем wp-env:

npm init -y
npm install @wordpress/env --save-dev
npm install phpunit/phpunit --save-dev

Создаем файл .wp-env.json для конфигурации тестового окружения:

{
    "core": "WordPress/WordPress#master",
    "phpVersion": "8.2",
    "plugins": [ "." ],
    "mappings": {
        "wp-content/themes/customcode": "./"
    }
}

Шаг 2: Настройка PHPUnit

Создаем конфигурационный файл phpunit.xml:

<?xml version="1.0"?>
<phpunit
    bootstrap="tests/bootstrap.php"
    backupGlobals="false"
    colors="true"
    convertErrorsToExceptions="true"
    convertNoticesToExceptions="true"
    convertWarningsToExceptions="true"
>
    <testsuites>
        <testsuite name="Unit">
            <directory prefix="test-" suffix=".php">./tests/</directory>
        </testsuite>
    </testsuites>
</phpunit>

Файл tests/bootstrap.php загружает ядро WordPress в память для тестов:

<?php
// Загрузка тестового окружения WP
$_tests_dir = getenv( 'WP_TESTS_DIR' ) ?: '/tmp/wordpress-tests-lib';
require_once $_tests_dir . '/includes/functions.php';

// Активируем нашу тему/плагин до загрузки WP
tests_add_filter( 'muplugins_loaded', function() {
    require dirname( __DIR__ ) . '/functions.php';
} );

require $_tests_dir . '/includes/bootstrap.php';

Шаг 3: Написание первого теста

Допустим, в теме есть функция расчета времени чтения статьи (которая использовалась в Headless-архитектуре). Создаем файл tests/test-reading-time.php:

<?php
class Test_Reading_Time extends WP_UnitTestCase {

    public function test_empty_content() {
        $time = customcode_calculate_reading_time( '' );
        $this->assertEquals( 0, $time );
    }

    public function test_standard_reading_speed() {
        // Генерируем текст из 400 слов (при скорости 200 слов/мин = 2 минуты)
        $text = str_repeat( 'word ', 400 );
        $time = customcode_calculate_reading_time( $text );
        $this->assertEquals( 2, $time );
    }

    public function test_html_tags_stripped() {
        // HTML-теги не должны учитываться в подсчете слов
        $text = '<h1>Заголовок</h1><p>' . str_repeat( 'word ', 200 ) . '</p>';
        $time = customcode_calculate_reading_time( $text );
        $this->assertEquals( 1, $time );
    }
}

Запуск тестов:

# Запускаем тестовый контейнер
npx wp-env start

# Выполняем PHPUnit внутри контейнера
npx wp-env run phpunit "vendor/bin/phpunit"

Уровень 2: E2E тесты интерфейса через Playwright

PHPUnit не проверит, что JavaScript-меню открывается по клику, или что форма контактной формы реально отправляет письма (без моков). Для этого используется Playwright от Microsoft — современный инструмент для браузерной автоматизации.

Шаг 1: Инициализация Playwright

npm init playwright@latest
# Выбираем TypeScript, папку tests/e2e, включаем GitHub Actions workflow

В файле playwright.config.ts указываем базовый URL нашего локального wp-env:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests/e2e',
  fullyParallel: true,
  retries: 1,
  use: {
    baseURL: 'http://localhost:8888', // Стандартный порт wp-env
    trace: 'on-first-retry',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'Mobile Safari', use: { ...devices['iPhone 13'] } },
  ],
  webServer: {
    command: 'npm run wp-env start',
    url: 'http://localhost:8888',
    reuseExistingServer: !process.env.CI,
  },
});

Шаг 2: Тест авторизации и публикации поста

Создаем файл tests/e2e/admin-flow.spec.ts. Этот сценарий проверяет, что администратор может войти в панель и создать черновик:

import { test, expect } from '@playwright/test';

test.describe('WordPress Admin Flow', () => {
  
  test.beforeEach(async ({ page }) => {
    // Авторизация перед каждым тестом
    await page.goto('/wp-login.php');
    await page.fill('#user_login', 'admin');
    await page.fill('#user_pass', 'password');
    await page.click('#wp-submit');
    await expect(page).toHaveURL(/wp-admin/);
  });

  test('should create a new draft post', async ({ page }) => {
    await page.goto('/wp-admin/post-new.php');
    
    // Заполняем заголовок (учитываем блочный редактор Gutenberg)
    const titleField = page.locator('h1[contenteditable="true"], textarea[aria-label="Add title"]').first();
    await titleField.fill('Тестовая статья Playwright');
    
    // Сохраняем как черновик
    await page.click('button:has-text("Save draft")');
    
    // Проверяем, что появилось уведомление об успешном сохранении
    await expect(page.locator('.components-snackbar')).toBeVisible();
  });

  test('frontend should render critical CSS correctly', async ({ page }) => {
    await page.goto('/');
    
    // Проверяем, что шапка загрузилась и имеет правильный цвет фона (VS Code style)
    const header = page.locator('.title-bar');
    await expect(header).toBeVisible();
    await expect(header).toHaveCSS('background-color', 'rgb(45, 45, 48)'); // #2d2d30
  });
});

Уровень 3: Интеграция в CI/CD (GitHub Actions)

Тесты должны запускаться автоматически при каждом Pull Request, блокируя мерж кода, который ломает функционал. Интеграция с GitHub Actions описана в руководстве по настройке CI/CD, здесь приводится специфичный workflow для тестов.

Создаем файл .github/workflows/tests.yml:

name: WordPress Tests

on:
  pull_request:
    branches: [ main, develop ]

jobs:
  phpunit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node & wp-env
        run: |
          npm ci
          npm run wp-env start
          
      - name: Run PHPUnit
        run: npm run wp-env run phpunit "vendor/bin/phpunit --testdox"

  playwright:
    runs-on: ubuntu-latest
    needs: phpunit
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 20
          
      - name: Install dependencies
        run: npm ci
        
      - name: Install Playwright Browsers
        run: npx playwright install --with-deps
        
      - name: Start WP Environment
        run: npm run wp-env start
        
      - name: Run Playwright tests
        run: npx playwright test
        
      - uses: actions/upload-artifact@v4
        if: ${{ !cancelled() }}
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 7

Тестирование REST API и Headless-сценариев

Для проектов, использующих WordPress как headless CMS, критично тестировать JSON-ответы. Playwright отлично справляется с проверкой API:

test('REST API should return posts with custom fields', async ({ request }) => {
  const response = await request.get('/wp-json/wp/v2/posts?per_page=1');
  expect(response.ok()).toBeTruthy();
  
  const posts = await response.json();
  expect(posts.length).toBeGreaterThan(0);
  
  // Проверяем наличие кастомного поля, зарегистрированного через register_rest_field
  expect(posts[0]).toHaveProperty('custom_fields');
});

Чек-лист настройки тестирования

  • ✅ Установлен @wordpress/env для изолированного тестового окружения
  • ✅ Настроен phpunit.xml и написаны базовые unit-тесты для кастомных функций
  • ✅ Инициализирован Playwright с конфигурацией под локальный WP
  • ✅ Написаны E2E-сценарии для критичных путей (логин, публикация, отправка формы)
  • ✅ Добавлены проверки CSS-свойств для предотвращения регрессий верстки (CLS)
  • ✅ Настроен GitHub Actions workflow для автозапуска тестов на каждый PR
  • ✅ Тесты REST API покрывают кастомные endpoints

Решение частых проблем

Тесты «протекают» (состояние БД сохраняется между прогонами)

PHPUnit в WordPress по умолчанию оборачивает каждый тест в транзакцию MySQL и откатывает её в конце. Если используются нестандартные движки таблиц (например, MyISAM) или внешние API-вызовы, транзакции не работают. Решение: использовать setUp() и tearDown() для ручного удаления созданных постов и опций через wp_delete_post() и delete_option().

Playwright не может авторизоваться из-за 2FA или Captcha

Плагины безопасности (Wordfence, iThemes) часто блокируют автоматические входы. В тестовом окружении wp-env эти плагины не должны быть активированы. Если тестируется прод-копия, используйте механизм storageState в Playwright: один раз авторизуйтесь вручную, сохраните куки в JSON и подгружайте их в тестах, обходя страницу логина.

Тесты падают из-за медленного рендеринга (Flaky tests)

Если Playwright пытается кликнуть по кнопке до того, как она отрисовалась, тест падает. Избегайте жестких задержек (page.waitForTimeout). Вместо этого используйте умные ожидания: await expect(page.locator('.button')).toBeEnabled(); или await page.waitForLoadState('networkidle');.

Как тестировать отправку email (SMTP)?

Не отправляйте реальные письма в тестах. Перехватывайте вызовы wp_mail() через мокирование или используйте плагин WP Mail Logging в тестовом окружении, а в Playwright проверяйте наличие записи в логе через прямой SQL-запрос к тестовой БД.

Итог

Автоматическое тестирование WordPress перестает быть уделом энтерпрайз-проектов. Связка @wordpress/env, PHPUnit и Playwright разворачивается за 15 минут и полностью закрывает риски регрессий. Unit-тесты гарантируют, что PHP-логика работает корректно, а E2E-тесты подтверждают, что пользовательский интерфейс не сломан после очередного обновления плагинов. Интеграция этих инструментов в CI/CD пайплайн превращает деплой из стресса в рутинную, предсказуемую процедуру.