Подтвердить что ты не робот

Как уменьшить количество подключений к базе данных в тестах в PHPUnit и ZF3?

Я пишу тесты интеграции/базы данных для приложения Zend Framework 3 с помощью

  • zendframework/zend-test 3.1.0,
  • phpunit/phpunit 6.2.2 и
  • phpunit/dbunit 3.0.0

Мои тесты не работают из-за

Connect Error: SQLSTATE[HY000] [1040] Too many connections

Я установил некоторые точки останова и заглянул в базу данных:

SHOW STATUS WHERE `variable_name` = 'Threads_connected';

И я действительно видел через 100 открытые соединения.

Я сократил их, отключив в tearDown():

protected function tearDown()
{
    parent::tearDown();
    if ($this->dbAdapter && $this->dbAdapter instanceof Adapter) {
        $this->dbAdapter->getDriver()->getConnection()->disconnect();
    }
}

Но у меня все еще есть 80 открытые соединения.

Как уменьшить количество подключений базы данных в тестах до минимального?


подробнее

(1) У меня много тестов, где я dispatch URI. Каждый такой запрос вызывает по крайней мере один запрос базы данных, который вызывает новое подключение к базе данных. Эти соединения, похоже, не закрыты. Это может вызвать большинство соединений. (Но я еще не нашел способ закрыть приложение после того, как запрос обработан.)

(2) Одной из проблем может быть мое тестирование против базы данных:

protected function retrieveActualData($table, $idColumn, $idValue)
{
    $sql = new Sql($this->dbAdapter);
    $select = $sql->select($table);
    $select->where([$table . '.' . $idColumn . ' = ?' => $idValue]);
    $statement = $sql->prepareStatementForSqlObject($select);
    $result = $statement->execute();
    $data = $result->current();
    return $data;
}

Но вызов $this->dbAdapter->getDriver()->getConnection()->disconnect() перед return ничего не дал.

Пример использования в методе тестирования:

public function testInputDataActionSaving()
{
    // The getFormParams(...) returns an array with the needed input.
    $formParams = $this->getFormParams(self::FORM_CREATE_CLUSTER);

    $createWhateverUrl = '/whatever/create';
    $this->dispatch($createWhateverUrl, Request::METHOD_POST, $formParams);

    $this->assertEquals(
        $formParams['whatever']['some_param'],
        $this->retrieveActualData('whatever', 'id', 2)['some_param']
    );
}

(3) Еще одна проблема может возникнуть в PHPUnit (или моей конфигурации?). (Striken out, потому что "PHPUnit не делает ничего, связанного с подключениями к базе данных". см. этот.) В любом случае, даже если это не проблема PHPUnit, факт в том, что после строки

$testSuite = $configuration->getTestSuiteConfiguration($this->arguments['testsuite'] ?? null);

в PHPUnit\TextUI\Command Я получаю 31 новые соединения.

4b9b3361

Ответ 1

Чистый и правильный подход

Кажется, это проблема, если "ваш код написан таким образом, который трудно проверить". Соединение с БД должно обрабатываться DIC или (в случае некоторого пула соединений) некоторый класс специализации. В принципе, класс, содержащий retrieveActualData(), должен иметь экземпляр Sql, который передается как зависимость в конструкторе.

Вместо этого выглядит, что ваш класс Sql является вредоносной оболочкой PDO, которая (скорее всего) установила соединение с БД всякий раз, когда вы создаете экземпляр. Вместо этого вы должны делиться одним и тем же экземпляром PDO между несколькими классами. Таким образом, вы можете контролировать количество установленных соединений и иметь возможность проверить ваш код в (некоторой) изоляции.

Итак, основное решение - ваш код плохой, но вы можете его очистить.

Вместо того, чтобы иметь new фрагменты, посыпленные глубоко в вашем дереве выполнения, передать соединение как зависимость и поделиться им.

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

В случае логики с привязкой к DB и gremlins

Но есть более практичный аспект, который вы должны учитывать. Используйте SQLite вместо реальной базы данных в своих тестах интеграции. PDO поддерживает этот вариант (вам просто нужно предоставить другой DSN для вашего тестового кода).

Если вы переключитесь на использование SQLite в качестве "тестовой базы данных", вы сможете иметь четко определенные состояния БД (несколько), против которых вы можете протестировать свой код.

У вас есть что-то вроде файла integration-002.db, который содержит подготовленное состояние базы данных. В начальной загрузке ваших интеграционных тестов вы просто скопируете эти подготовленные SQL файлы sqlite с integration-0902.db на live-002.db и запустите все тесты.

use PHPUnit\Framework\TestCase;

final class CombinedTest extends TestCase
{
    public static function setUpBeforeClass()
    {
        copy(FIXTURE_PATH . '/integration-02.db', FIXTURE_PATH . '/live-02.db');
    }


    // your test go here

}

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

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

Этот подход можно увидеть на практике в этом проекте.


P.S. из личного опыта - использование SQLite в тестах интеграции также улучшает общее качество SQL-кода (если вы не используете построители запросов, а вместо этого пишете пользовательские data-mappers). Потому что это заставляет вас учитывать различия между доступной функциональностью в SQLite против MariaDB или PostgreSQL. Но это один из тех, "ваш пробег может меняться".

P.P.S., вы можете использовать оба предложенных подхода в одно и то же время, поскольку они будут только усиливать друг друга.

Ответ 2

Возможно, вы настроили PHP/DB для использования постоянных подключений. Это происходит только тогда, когда эти соединения остаются после завершения теста. Это не так уж плохо.

Из руководства: Постоянные соединения - это ссылки, которые не закрываются при завершении выполнения script. Когда запрашивается постоянное соединение, PHP проверяет, существует ли уже идентичное постоянное соединение (которое осталось открытым ранее), и если оно существует, оно использует его.

После того, как вы установили соединение с [email protected]:port, ваша вещь и отключились (выполнение конца сеанса), после этого снова подключитесь к тому же [email protected]:port, независимо от того, какие таблицы используются, вы будете подключены через тот же разъем подключения.

Четыре возможных причины вашей проблемы

  • поскольку вы используете разные пользователи для подключения к серверу db
  • потому что вы передаете имена таблиц в соединение
  • потому что вы запускаете сразу несколько тестов.
  • потому что вы создаете несколько соединений

и наиболее вероятным является 4-й, потому что его соблазн создать функцию frabric для создания дескриптора db каждый раз, когда вам нужна база данных, которая создает новое соединение:

function getConnection() {
    // This is an example to test, that it do leave behind a non closed connection. 
    // Skip "p:", to reduce connections left unless you are configured
    // globally for persistency, eg. by mysqlnd.
    //                      p: forced persistency
    $link = mysqli_connect("p:127.0.0.1", "my_user", "my_password", "my_db");

    if (!$link) return false;

    return $link;
}

Дело в том, что для каждого вызова примерного метода по тому же потоку будет открыто совершенно новое соединение, потому что вы действительно просите об этом. Стойкие сокеты повторно используются, только если они больше не используются (создатель script завершает свое выполнение ранее). (по крайней мере, именно так я научился использовать их несколько лет назад)

Чтобы избежать создания слишком большого количества подключений, необходимо восстановить соединение factory, чтобы сохранить все отдельные подключения и доставлять те ссылки, которые вы хотите по требованию, без повторного вызова конструктора соединений. Таким образом, для конкретного пользователя на частном сервере вы, наконец, запускаете один раз, например. mysqli_connect для получения постоянного соединения с сервером и повторного использования его до конца выполнения script.

class  db
{

    static $storage = array();

    public static function getConnection($username = 'username') {

        if (!array_key_exists($username, self::$storage) {
            $link = mysqli_connect("p:127.0.0.1", $username, "my_password", "my_db");

            if (!$link) return false;

            self::$storage[$username] = $link;
        }

        return self::$storage[$username];
    }
}

// ---
$a = db::getConnection();
$b = db::getConnection();

// both $a and $b are the same connection, using the same socket on your server
var_dump($a, $b);

Возвращаясь к приведенным примерам, возможно, это из-за строки:

$sql = new Sql($this->dbAdapter);

выполняются снова и снова по вашим тестам или самим водителем делают что-то необычное при частом повторном использовании. Мой вопрос будет в том случае, если драйвер не создает новое соединение каждый раз при запуске getConnection(), или если конструктор Sql() не создает новое соединение при каждом вызове с помощью new Sql.

изменить 1 - после просмотра кода zf3:

Попробуйте выполнить поиск, если код не делает что-то вроде упорный пример. Но с использованием ZF3 я предпочел бы, что вы используете какое-то расширение, такое как mysqlnd, которое заставляет вас не использовать собственный драйвер mysql в пользу Streams со своими таймаутами.

edit 2 - db test один за другим:

Несмотря на стойкость сокетов, вы не можете использовать их вообще: серверу SQL требуется время, чтобы полностью отключить пользователя и освободить сокет для нового подключения. Если вы быстро запускаете тесты один за другим, это означает, что каждый тест запускается и уничтожается - что может привести к созданию нового соединения при каждом запуске любого вызова setUp() или загрузки загрузочного файла. Запустив загрузку тестов, которые создают экземпляр службы БД (что-нибудь, что вызовет Adapter/PDO/Conncetion::connect(), вы можете создать огромную очередь подключения, которая будет закрыта внизу вашей одноразовой. Это будет где настройка для Сопротивление сокета должно решить вашу проблему.

Ответ 3

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

Ваша функция tearDown() предполагает, что подключение к базе данных действительно находится внутри вашей функции setUp(), которая будет подключаться к ней один раз за каждый тест. Если ваш код подключения использует PDO::ATTR_PERSISTENT, и вы настраиваете, как указано выше, выньте его, вы хотите, чтобы необработанные соединения умирали.

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