Статьи

Настройка очистки базы данных Codeception

Недавно я искал способы ускорить выполнение набора тестов на Blopboard . Мы используем инфраструктуру Codeception для написания функциональных тестов для нашего REST API, часть которых влечет за собой перевод базы данных в известное состояние с использованием модуля Db Codeception . Поведение этого модуля аналогично поведению расширения базы данных PHPUnit за одним исключением: где PHPUnit только усекает таблицы и оставляет их схемы нетронутыми, Codeception удаляет структуру базы данных и ожидает, что дамп SQL используется для воссоздания его между тестами.

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

<?php
 
namespace Codeception\Module;
 
/**
 * Extends the standard Db helper to override cleanup behavior so that tables
 * are truncated rather than dropped and recreated between tests.
 */
class DbHelper extends \Codeception\Module\Db
{
    protected function cleanup()
    {
        $dbh = $this->driver->getDbh();
        if (! $dbh) {
            throw new ModuleConfigException(
                __CLASS__,
                "No connection to database. Remove this module from config if you don't need database repopulation"
            );
        }
 
        try {
            if (! count($this->sql)) {
                return;
            }
 
            /** Start **/
            $dbh->exec('SET FOREIGN_KEY_CHECKS=0;');
            $res = $dbh->query("SHOW FULL TABLES WHERE TABLE_TYPE LIKE '%TABLE';")->fetchAll();
            foreach ($res as $row) {
                $dbh->exec('TRUNCATE TABLE `' . $row[0] . '`');
            }
            $dbh->exec('SET FOREIGN_KEY_CHECKS=1;');
            /** End **/
 
        } catch (\Exception $e) {
            throw new ModuleException(__CLASS__, $e->getMessage());
        }
    }
}

Вышеупомянутый класс модуля используется вместо Dbмодуля . Чтобы придумать это, я начал копаться в логике самого  Dbкласса модуля. Codeception имеет несколько методов подключения для модулей, которые он вызывает внутри. Одним из них является то _initialize(), что вызывается после создания экземпляра класса модуля и загрузки конфигурации для него, но до запуска любых тестов.

Глядя на _initialize()реализации в Dbклассе модуля, я обнаружил , что она делает вызов к способу получения объекта драйвера для конкретных используемой базы данных. Этот объект драйвер реализует метод , что собственный модуль класса  метода вызовы между тестами для обработки сброса состояния базы данных.cleanup()Dbcleanup()

Однако здесь есть проблема: вызов для получения объекта драйвера является статическим методом, что означает, что я не могу указать свою собственную логику получения объекта драйвера, а не логику, которую Codeception использует по умолчанию. Это препятствует расширяемости, а также тестируемости .

Я мог бы обойти это, расширив Dbкласс модуля и переопределив его _initialize()метод для вызова другого кода, чтобы получить экземпляр моего собственного класса драйвера. Однако это означало бы дублирование большей части логики этого метода, который не имеет тривиального размера. Это повысило бы вероятность того, что мой код не будет работать с последующими версиями Codeception, если метод, который я переопределил, изменился.

В конце я нашел альтернативу — расширить Dbкласс модуля и переопределить его cleanup()метод. Хотя это по- прежнему приводит к дублированию кода, код дублируется (который разграничены /** Start **/и /** End **/комментарии в приведенном выше примере кода) короче, проще и менее вероятно, будет изменено таким образом, что это влияет функциональность моего кода. Однако стоит отметить, что приведенный выше пример кода, вероятно, будет работать только с MySQL и потребует модификаций для работы с другими серверами баз данных.

Если бы Dbкласс модуля инкапсулировал свой вызов Driver::create()внутри метода экземпляра, я мог бы просто переопределить этот метод в своем подклассе и найти более чистое решение.

В качестве альтернативы, Codeception мог бы поддержать такое решение:

<?php
 
namespace Codeception\Module\Db;
 
interface DriverFactoryInterface
{
  public function create($dsn, $user, $password);
 
  // ...
}
 
class DriverFactory implements DriverFactoryInterface
{
  public function create($dsn, $user, $password)
  {
    // The contents of Driver::create() would go here.
  }
}
 
namespace Codeception\Module;
 
class Db extends \Codeception\Module
{
  protected $driverFactory;
  protected $driver;
 
  public function _initialize()
  {
    // ...
    if (!isset($this->config['driverFactoryClass'])) {
      $this->config['driverFactoryClass'] = '\Codeception\Module\Db\DriverFactory';
    }
    $this->driver = $this->getDriverFactory()->create(
      $this->config['dsn'],
      $this->config['user'],
      $this->config['password']
    );
    // ...
  }
 
  public function getDriverFactory()
  {
    if (!$this->driverFactory) {
      $driverFactoryClass = $this->config['driverFactoryClass'];
      $this->setDriverFactory(new $driverFactoryClass);
    }
    return $this->driverFactory;
  }
 
  public function setDriverFactory(DriverFactoryInterface $driverFactory)
  {
    $this->driverFactory = $driverFactory;
  }
 
  // ...
}

В приведенном выше решении есть DriverFactoryInterfaceинтерфейс, среди прочего, с create()методом экземпляра и DriverFactoryклассом, который реализует этот интерфейс. Класс Dbмодуля позволяет определить класс, который реализует этот интерфейс через его конфигурацию. Затем он обрабатывает создание экземпляра этого класса и вызывает create()метод этого объекта из его _initialize()метода, а не вызывает, Driver::create()как это происходит в настоящее время. Имея этот код, я мог бы написать свой собственный класс, реализующий интерфейс для возврата моего собственного драйвера. Это позволило бы мне достичь своей цели, не прибегая к подклассам.

В любом случае, моя догадка и решение окупились: благодаря этому решению мы смогли сократить время выполнения нашего набора тестов примерно на 30%. Другим приятным побочным эффектом стало то, что мне больше не нужно было поддерживать копию схемы нашей базы данных, кроме той, которую мы уже поддерживаем с помощью Liquibase .

Я надеюсь, что это решение и мои мысли о нынешнем дизайне Codeception будут полезны для кого-то. Спасибо за чтение.