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

16

Я пишу тесты интеграции / базы данных для приложения 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 (или моей конфигурации?). (Вычеркнуто, потому что «PHPUnit не делает ничего, связанного с соединениями с базой данных.», см. это comment.) В любом случае, даже если это не проблема PHPUnit, факт в том, что после строки

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

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

    
задан automatix 12.08.2017 в 16:05
источник
  • PHPUnit не делает ничего, связанного с подключением к базе данных. –  Sebastian Bergmann 12.08.2017 в 17:35
  • Можете ли вы включить один из ваших фактических тестов? (Я предполагаю, что retrieveActualData () используется для проверки чего-то, потому что он ничего не тестирует.) –  Jory Geerts 15.08.2017 в 14:40
  • Выполнено. См. TestInputDataActionSaving () в вопросе. Благодарю. –  automatix 15.08.2017 в 15:40
  • Runt it with --process-isolation. Он будет закрывать все соединения после каждого теста. –  Alex Blex 15.08.2017 в 16:32
  • Спасибо, просто попробовал ($ vendor / phpunit / phpunit / phpunit --process-isolation --no-coverage --configuration ./phpunit.xml). Выполнение прерывается с ошибкой. Примечание: Исключение: Сериализация «Закрытие» не разрешена в /var/www/.../my-project/vendor/phpunit/phpunit/src/Util/GlobalState.php:170. Для параметра BackupGlobals установлено значение false, а Bootstrap не содержит замыканий. –  automatix 15.08.2017 в 21:52
Показать остальные комментарии

3 ответа

8

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

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

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

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

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

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

В случае логики, связанной с БД и гремлинов

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

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

У вас есть что-то вроде файла integration-002.db , который содержит подготовленное состояние базы данных. Во время начальной загрузки ваших интеграционных тестов вы просто копируете подготовленные файлы базы данных 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 (если вы не используете построители запросов, а вместо этого пишете собственные средства отображения данных). Потому что это заставляет вас учитывать различия между доступной функциональностью в SQLite по сравнению с MariaDB или PostgreSQL. Но это одна из тех вещей, которые «могут варьироваться».

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

ответ дан tereško 21.08.2017 в 01:09
  • Говорить, что «ваш код плох», не дает решения вообще. Также вы не видели его код. –  yergo 22.08.2017 в 09:36
  • Код был приведен в качестве примера в retrieveActualData (), а «ваш код плох» в качестве сводки после решения. –  tereško 22.08.2017 в 09:49
3

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

From manual: Persistent connections are links that do not close when the execution of your script ends. When a persistent connection is requested, PHP checks if there's already an identical persistent connection (that remained open from earlier) - and if it exists, it uses it.

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

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

  1. потому что вы используете разных пользователей для подключения к серверу базы данных
  2. потому что вы доставляете имена таблиц в соединение
  3. потому что вы запускаете несколько тестов одновременно
  4. потому что вы создаете несколько соединений

и наиболее вероятным является 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;
}

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

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

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 .

edit 1 - после просмотра кода zf3:

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

редактировать 2 - проверять дБ один за другим:

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

    
ответ дан yergo 21.08.2017 в 10:37
  • Если вам нужно настроить постоянное соединение только для теста, вы решаете неправильную проблему –  tereško 21.08.2017 в 17:19
2

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

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

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

    
ответ дан Paul Stanley 21.08.2017 в 10:49