У меня есть проект Symfony 4.2 со следующей структурой:
Я использую класс базы данных, который внутри создает соединение PDO. Если я запускаю свои тесты с PHPUnit, мой класс базы данных должен переключиться с mysql на sqlite. Здесь все работает нормально.
Я не могу сохранить однажды созданный экземпляр базы данных при запуске только теста один. Symfony, кажется, воссоздает его во время теста: внутри шаблона Twig, который использует расширение Twig. Поскольку класс базы данных использует
new \PDO('sqlite::memory:');
он теряет созданные таблицы, и поэтому тест не проходит. Я знаю, что экземпляр базы данных (со ссылкой на PDO) сбрасывается после каждого теста, но в моей ситуации у меня есть только один тест. Как я могу убедиться, что он повторно использует экземпляр базы данных?
Класс InstanceExtension предоставляет заголовок функции, которая используется в шаблоне Twig и должна обращаться к базе данных.
<?php
namespace App\TwigExtension;
use App\Service\Database;
use Twig\TwigFunction;
use Twig\Extension\AbstractExtension;
class InstanceExtension extends AbstractExtension
{
protected $db;
/**
* @param Database $db
*/
public function __construct(Database $db)
{
$this->db = $db;
}
/**
* Tries to return the title/name for a given ID.
*
* @param string $subject
* @param string|array $tables
* @param string $lang Default is 'de'
*
* @return string label or id, if no title/name was found
*/
public function title(string $subject, $tables, string $lang = 'de'): string
{
return $this->db->get_instance_title($subject, $tables, $lang);
}
}
В моем services.yaml класс Database установлен как общедоступный (что должно разрешить его повторное использование, не так ли?):
App\Service\Database:
public: true
Вот часть класса базы данных, которая инициализирует соединение PDO. Рабочий код, который вместо этого использует MySQL, удален:
<?php
declare(strict_types=1);
namespace App\Service;
class Database
{
/**
* @param Config $app_config
*
* @throws \Exception
*/
public function __construct(Config $app_config)
{
global $sqlite_pdo;
try {
// non test
// ..
// test environment
} else {
$this->db_engine = 'sqlite';
// workaround to ensure we have the same PDO instance everytime we use the Database instance
// if we dont use it, in Twig extensions a new Database instance is created with a new SQLite
// database, which is empty.
if (null == $sqlite_pdo) {
$pdo = new \PDO('sqlite::memory:');
$sqlite_pdo = $pdo;
} else {
$pdo = $sqlite_pdo;
}
}
} catch (\PDOException $e) {
if (\strpos((string) $e->getMessage(), 'could not find driver') !== false) {
throw new \Exception(
'Could not create a PDO connection. Is the driver installed/enabled?'
);
}
if (\strpos((string) $e->getMessage(), 'unknown database') !== false) {
throw new \Exception(
'Could not create a PDO connection. Check that your database exists.'
);
}
// Don't leak credentials directly if we can.
throw new \Exception(
'Could not create a PDO connection. Please check your username and password.'
);
}
if ('mysql' == $this->db_engine) {
// ...
}
// default fetch mode is associative
$pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC);
// everything odd will be handled as exception
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$this->db = $pdo;
}
// ...
}
Один тест выглядит примерно так:
<?php
class SalesChannelControllerTest extends TestCase
{
public function setUp()
{
parent::setUp();
// init Database class, using SQLite
$this->init_db();
// further setup function calls
// SalesChannelController is a child of
// Symfony\Bundle\FrameworkBundle\Controller\AbstractController
$this->fixture = new SalesChannelController(
$this->db
// ...
);
}
/**
* Returns a ready-to-use instance of the database. Default adapter is SQLite.
*
* @return Database
*/
protected function init_db(): Database
{
// config parameter just contains DB credentials
$this->db = new Database($this->config);
}
public function test_introduction_action()
{
// preparations
/*
* run action
*
* which renders a Twig template, creates a Response and returns it.
*/
$response = $this->fixture->introduction_action($this->request, $this->session, 'sales_channel1');
/*
* check response
*/
$this->assertEquals(200, $response->getStatusCode());
}
}
Мой текущий обходной путь заключается в том, чтобы сохранить экземпляр PDO в глобальной переменной и повторно использовать его, если это необходимо.
<?php
global $sqlite_pdo;
// ...
// inside Database class, when initializing the PDO connection
if (null == $sqlite_pdo) {
$pdo = new \PDO('sqlite::memory:');
$sqlite_pdo = $pdo;
} else {
$pdo = $sqlite_pdo;
}
Если вам нужна дополнительная информация, пожалуйста, сообщите мне. Спасибо за ваше время и помощь заранее!
Привет @dbrumann, я обновлю свой первоначальный вопрос.
Это уже может помочь сохранить фикстуру внутри статической переменной, а затем использовать public static function setupBeforeClass() вместо простой настройки. Это сохранит прибор между каждым тестом. Дайте мне знать, если это поможет или вам нужен полный пример. Это все еще может зависеть от того, что делает init_db. Также было бы полезно увидеть содержимое этой функции.
Привет @dbrumann, установкаBeforeClass возможна. Но мне было интересно, могу ли я настроить его в самой Symfony. Любая идея?
Насколько я могу судить, ничего не связано с вашим приложением Symfony, например. загрузка ядра и получение соединения из контейнера в вашем тесте. Если вам нужна тестовая база данных, которую можно использовать во всем наборе тестов, я обычно делаю сценарий начальной загрузки, который создает sqlite-файл в моей папке var/, использует команды Doctrine для построения схемы, а затем указывает DATABASE_URL, используемый Symfony в этот файл. Для примера см. этот демонстрационный проект: github.com/dbrumann/todo-basic/blob/master/tests/bootstrap.php
Как я уже писал в своем первоначальном вопросе, он сбрасывает соединение PDO при рендеринге шаблона Twig. Это делается в действии контроллера. Symfony участвует большую часть времени. Части, не относящиеся к Symfony, работают нормально. Поскольку база данных — это служба, мне было интересно, как настроить ее для повторного использования без повторного создания экземпляра.
Если бы этот материал базы данных был службой, он повторно использовался бы без повторного создания. Почему бы вам не использовать эту услугу в тестовом примере?
Я думал, что сделал. Пожалуйста, посмотрите в моем вопросе InstanceExtension, у которого Database является первым параметром. Это класс, который обрабатывает вещи PDO. InstanceExtension предоставляет функцию title, которую я использую в шаблоне ветки. Этот шаблон отображается в моем тестовом примере с помощью действия контроллера, называемого introduction_action (от контроллера SalesChannelController, дочернего элемента от Symfony\Bundle\FrameworkBundle\Controller\AbstractController). Но экземпляр базы данных повторно создается Symfony, по какой-то причине я не знаю.
Установка службы на public не должна здесь меняться, но что делает init_db?
Привет @NicoHaase, я обновлю свой первоначальный вопрос и добавлю эту информацию.
Что ж, теперь мы подходим к этому: почему вы создаете новый экземпляр этого класса базы данных при каждой установке? Почему бы тебе не использовать для этого свой контейнер?
Спасибо за быстрый ответ. Я не уверен, правильно ли я вас понял. Что вы подразумеваете под «использовать свой контейнер»? Есть ли здесь лучшая практика, которой я могу следовать?
Конечно, есть: когда вы используете контейнер (как вы это делаете, когда используете сервисы Symfony), вы не должны создавать экземпляры сервисов вручную, а извлекать их из контейнера.






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