Сайт Романа ПарпалакаБлог20230828

Как покрыть тестами устаревший код?

28 августа 2023 года, 12:22

Многим разработчикам приходилось поддерживать и дорабатывать устаревшие приложения, в которых никогда не было автотестов. С помощью приемочных тестов библиотеки Codeception можно покрыть веб-приложение на любом фреймворке и даже на любых языках. В предыдущей заметке о развитии движка S2 я вскользь упомянул о добавлении таких тестов.

Идея приемочных тестов в том, что приложение тестируется целиком, как есть. Для легаси-кода с процедурным подходом, хранением состояния в глобальных переменных, множеством точек выхода die/exit при ошибках и редиректах, перемешанной логикой и представлением чаще всего альтернатив нет.

Я расскажу на примере S2, как добавлять приемочные тесты в легаси-приложение. Ниже я приведу существенные упрощенные фрагменты кода и оставлю ссылки на полные версии файлов.

Проще всего Codeception подключить к проекту через composer:

    "require-dev": {
        "codeception/codeception": "^4.2",
        "codeception/module-asserts": "^2.0.0",
        "codeception/module-phpbrowser": "^2.0"
    }

В проекте нужно создать файл codeception.yml:

suites:
    acceptance:
        actor: AcceptanceTester
        modules:
            enabled:
                - Asserts
                - PhpBrowser:
                      url: http://localhost:8881
                      curl:
                          CURLOPT_TIMEOUT_MS: 120000

После этого файлы тестов можно писать в таком cest-синтаксисе:

<?php

use Codeception\Example;

class InstallCest
{
    public function tryToTest(AcceptanceTester $I, Example $example): void
    {
        $I->install('admin', 'passwd', $example['db_type'], $example['db_user'], $example['db_password']);

        $I->amOnPage('/');
        $I->see('Site powered by S2');
        $I->click(['link' => 'Page 1']);
        $I->see('If you see this text, the install of S2 has been successfully completed.');
        $I->canWriteComment();
    }
}

Здесь методы amOnPage(), see(), click() — встроенные, а install() и canWriteComment() — мои сокращения, определенные в классе-тестере:

<?php

use Codeception\Actor;

class AcceptanceTester extends Actor
{
    public function install(string $userName, string $userPass, string $dbType, string $dbUser, string $dbPassword): void
    {
        $I = $this;
        $I->amOnPage('/');
        $I->seeLink('install S2', '/_admin/install.php');
        $I->amOnPage('/_admin/install.php');
        $I->seeResponseCodeIs(200);
        $I->see('S2 2.0dev', 'h1');

        $I->selectOption('req_db_type', $dbType);
        $I->fillField('req_db_host', '127.0.0.1'); // not localhost for Github Actions
        $I->fillField('req_db_name', 's2_test');
        $I->fillField('db_username', $dbUser);
        $I->fillField('db_password', $dbPassword);
        $I->fillField('req_username', $userName);
        $I->fillField('req_password', $userPass);
        $I->click('start');
        $I->canSeeResponseCodeIs(200);
        $I->see('S2 is completely installed!');
    }

    public function canWriteComment(): void
    {
        $I = $this;

        $name = 'Roman 🌞';
        $I->fillField('name', $name);
        $I->fillField('email', 'roman@example.com');
        $I->fillField('text', 'This is my first comment! 👪🐶');
        $text = $I->grabTextFrom('p#qsp');
        preg_match('#(\d\d)\+(\d)#', $text, $matches);
        $I->fillField('question', (int)$matches[1] + (int)$matches[2]);
        $I->click('submit');

        $I->seeResponseCodeIs(200);
        $I->see($name . ' wrote:');
        $I->see('This is my first comment!');
    }
}

Теперь посмотрим, как это всё запускается. Я написал отдельный скрипт:

# Очистка тестовой базы данных
mysql -uroot --execute="DROP DATABASE IF EXISTS s2_test; CREATE DATABASE s2_test;"

# Запуск веб-сервера
APP_ENV=test \
 PHP_CLI_SERVER_WORKERS=2 \
 nohup php \
  -d "max_execution_time=-1" \
  -d "opcache.revalidate_freq=0" \
  -S localhost:8881 >/dev/null 2>&1 &

serverPID=$!

# Запуск тестов
php _vendor/bin/codecept run acceptance

pkill -P $serverPID # Убиваем воркеры PHP, образовавшиеся из-за PHP_CLI_SERVER_WORKERS
kill $serverPID

Перед запуском тестов поднимается встроенный в php веб-сервер, обрабатывающий запросы по адресу http://localhost:8881/. Веб-сервер запускает несколько воркеров в параллель (PHP_CLI_SERVER_WORKERS=2), так как движок в процессе установки обращается сам к себе, чтобы понять, какая схема перезаписи URL доступна. В процессе установки создается файл config.php. Чтобы PHP сразу видел изменения в этом файле, пришлось переопределить параметр из php.ini: opcache.revalidate_freq=0. Альтернатива — добавить sleep(), но я не хотел играться с ненадежными способами. Переменная окружения APP_ENV=test говорит движку, чтобы он вместо файла config.php создавал и использовал файл config.test.php. Это упрощает запуск и тестов и обычной версии для разработки из одной папки.

Достоинства получившегося способа написания приемочных тестов следующие. Устаревший код приложения практически не нужно дорабатывать, чтобы писать тесты. Так как приложение тестируется через HTTP API, внутренние изменения в приложении, не меняющие API, не требуют доработки тестов. Тесты запускаются где угодно, я даже добавил запуск тестов в github actions при каждом пуше веток.

Недостатки, как обычно, есть продолжения достоинств. Тесты покрывают только серверную часть, js-код нужно тестировать другими методами. Так как тестируется отдельное приложение, нет возможности заранее подготовить его состояние. Тесты получаются зависимыми друг от друга: если тесты запускаются подряд, то последующие тесты должны знать о состоянии, в котором его оставили предыдущие тесты. Поэтому возможна ситуация, когда при некоторых изменениях логики работы приложения придется переписывать много тестов.

Поделиться

Мысли о движке сайтов S2 Ctrl Неудачная попытка включить JIT в PHP

Читайте также

Экспонента
Смотрю лекции Алексея Савватеева по математике и получаю удовольствие.
2017
По мотивам нового движка блога
PHP меня радует такими вещами (хотя заслуги PHP в этом особой нет, это типичный синтаксис C): while ($row = mysql_fetch_row($result))
2007

Оставьте свой комментарий


Формулы на латехе: $$f(x) = x^2-\sqrt{x}$$ превратится в $$f(x) = x^2-\sqrt{x}$$.
Выделение текста: [i]курсивом[/i] или [b]жирным[/b].
Цитату оформляйте так: [q = имя автора]цитата[/q] или [q]еще цитата[/q].
Других команд или HTML-тегов здесь нет.

Записи