I followed sitepoints Testing Symfony Apps with a Disposable Database Tutorial.
I added Fixtures in my Testcase and no Errors appear during SetUp. If i add an Error in the Fixtures (e.g. leaving a nullable=false field empty) the Error is shown, so this code does definitely get executed.
My Config:
doctrine:
dbal:
default_connection: memory
connections:
memory:
driver: pdo_sqlite
memory: true
charset: UTF8
My SetUp in my WebTestCase:
protected function setUp() {
parent::setUp();
self::bootKernel();
DatabasePrimer::prime(self::$kernel);
$this->loadFixtures([
'AppBundle\DataFixtures\ORM\UserData',
'AppBundle\DataFixtures\ORM\ArtistData'
]);
}
Yet, in my WebTestCase it appears that no Tables exist.
The output throws a Doctrine Exception saying my table does not exist.
SQLSTATE[HY000]: General error: 1 no such table: my_user_table
If i switch to sql_lite in a file, everything works fine without any other changes:
dbal:
default_connection: file
connections:
file:
driver: pdo_sqlite
path: %kernel.cache_dir%/test.db
charset: UTF8
Anyone had success with said tutorial or using a sqlite memory db for unit tests and has any hints or ideas?
Update:
I changed my Setup to this to ensure the kernel is not shut down in between. It did not help:
parent::setUp();
$this->client = $this->getClient();
MemoryDbPrimer::prime(self::$kernel);
$this->loadFixtures([
'AppBundle\DataFixtures\ORM\UserData',
'AppBundle\DataFixtures\ORM\ArtistData'
]);
When you
$client->request(<METHOD>, <URL>);
which calls
Symfony\Bundle\FrameworkBundleClient::doRequest($request)
After the request the kernel is shutdown by default, and your in-memory database is trashed.
If you call
client->disableReboot();
in the setup() function of your test, this will behavior is disabled, and you can run the whole suite.
I assume you call createClient() in your test functions. The very first thing that createClient() does is call static::bootKernel(). This basically means that the kernel you booted in your setUp() gets shut down and a new kernel is booted, with a fresh instance of the memory SQLite database.
You can move the createClient() call into your setUp(), replacing the bootKernel(), to avoid this:
class MyTest extends WebTestCase
{
private $client = null;
public function setUp()
{
$this->client = static::createClient();
// prime database
}
public function testSomething()
{
$crawler = $this->client->request('GET', '/');
// ...
}
}
Related
PHPUnit 7.5.15 by Sebastian Bergmann and contributors.
Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException : You have requested a non-existent service "test.service_container". Did you mean this: "service_container"?
/opt/project/backend/vendor/symfony/symfony/src/Symfony/Component/DependencyInjection/Container.php:277
/opt/project/backend/vendor/symfony/symfony/src/Symfony/Component/DependencyInjection/Container.php:225
/opt/project/backend/tests/Functional/KernelAwareTest.php:49
/opt/project/backend/tests/Functional/FlightTaskManagement/AssignEmployeeToTaskTest.php:47
public function setUp(): void
{
global $kernel;
$this->kernel = TestKernel::get();
$kernel = $this->kernel;
$container = $this->kernel->getContainer();
if ($container === null)
throw new \InvalidArgumentException('Container can not be null.');
$this->container = $container->get('test.service_container');
// $this->container = $container->get('service_container');
/** #var Registry $doctrine */
$doctrine = $this->container->get('doctrine');
/** #var \Doctrine\ORM\EntityManager $manager */
$manager = $doctrine->getManager();
$this->entityManager = $manager;
$this->entityManager->beginTransaction();
if (!$this->container->initialized(WorkDistributionTransport::class)) {
$this->container->set(WorkDistributionTransport::class, new InMemoryTransport());
}
if (!$this->container->initialized(Configuration::class)) {
$this->container->set(Configuration::class, new TestConfiguration());
}
parent::setUp();
}
It fails at line
$this->container = $container->get('test.service_container');
Symfony is 4.1 but looks like not finished to update. I can't remember by what we decided that it was not finished to update from earlier version.
Not clear if that is the problem that it is not finished to update. Looks like in 4.0 there is no such service so thats why. But then how to make it appear here?
Or maybe I can use
$this->container = $container->get('service_container');
as with earlier versions? Just what is faster way?
I just tried using
$this->container = $container->get('service_container');
but I then get
Doctrine\DBAL\DBALException : An exception occured while establishing a connection to figure out your platform version.
You can circumvent this by setting a 'server_version' configuration value
But I had set the version in config_test.yml so not clear which way is faster to fix.
doctrine:
dbal:
server_version: 5.7
driver: pdo_mysql
host: db_test
port: null
dbname: project
user: project
password: project
Probably if I load service_container then it does not load test config and thats why I get this server_version error. So then need to somehow make it load test config.
Found: I had hardcoded dev environment in AppKernel. Probably thats why I was getting this error. Hardcoding test env fixed. Would be good somehow to make it without hardcoding, but it is still better than nothing:
public function __construct($environment, $debug)
{
// $environment ='dev';
$environment ='test';
$debug= true;
parent::__construct($environment, $debug);
date_default_timezone_set('UTC');
}
I have a multi-tenant Symfony 5 application that consists in a general database which is responsible with global data and a dynamic amount of tenant databases, using the same schema among them, that are responsible with tenant specific data storage.
In total I have 2 entity managers, default that is connected to the general database and tenant that uses a wrapper (MultiDbConnectionWrapper) that can switch between any of the tenants databases.
config/doctrine.yaml
doctrine:
dbal:
connections:
default:
url: '%env(resolve:DATABASE_URL_CORE)%'
tenant:
url: '%env(resolve:DATABASE_URL_TENANT)%'
wrapper_class: App\Dbal\MultiDbConnectionWrapper
orm:
auto_generate_proxy_classes: true
entity_managers:
default:
connection: default
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
mappings:
Core:
is_bundle: false
type: annotation
dir: '%kernel.project_dir%/src/Entity/Core'
prefix: 'App\Entity\Core'
alias: Core
tenant:
connection: default
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
mappings:
Tenant:
is_bundle: false
type: annotation
dir: '%kernel.project_dir%/src/Entity/Tenant'
prefix: 'App\Entity\Tenant'
alias: tenant
For each EM, I'm using a different directory for migrations as well as a different migration config file/
config/migrations/tenant_migrations.yaml
migrations_paths:
'TenantMigrations': 'migrations/tenant'
em: 'tenant'
config/migrations/core_migrations.yaml
migrations_paths:
'CoreMigrations': 'migrations/core'
em: 'default'
My issue is that I'm trying to create a command that will cycle through all of the tenants databases and will run the migrations on each one. In my main database I have a table called tenants where I have stored the name of that tenant database.
To do so, I created the following command:
src/Command/MigrateTenantsCommand.php
protected function execute(InputInterface $input, OutputInterface $output): int
{
$tenants = $this->tenantRepository->findAll();
$connection = $this->tenantEntityManager->getConnection();
foreach ($tenants as $tenant){
$db = $tenant->getDatabaseUuid();
$connection->selectDatabase($db);
$this->getApplication()
->find('doctrine:migrations:migrate')
->run(new ArrayInput([
'--configuration' => 'config/migrations/tenant_migrations.yaml',
'--no-interaction' => true
]),$output);
}
return Command::SUCCESS;
}
The problem is that if I have at least 2 tenants stored in database and the command doctrine:migrations:migrate has to run twice, at the second run it throws the following error:
In FrozenDependencies.php line 13:
The dependencies are frozen and cannot be edited anymore.
From what I've tried it seems that somehow it is not related to the fact that I'm changing databases at $connection->selectDatabase($db);, but the fact that I cannot run twice the doctrine:migrations:migrate inside a command.
I've tried running also the following command and I received the same error:
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->getApplication()
->find('doctrine:migrations:migrate')
->run(new ArrayInput([
'--configuration' => 'config/migrations/core_migrations.yaml'
]),$output);
$this->getApplication()
->find('doctrine:migrations:migrate')
->run(new ArrayInput([
'--configuration' => 'config/migrations/core_migrations.yaml'
]),$output);
return Command::SUCCESS;
}
Anyone got any ideea why this happens? Thank you!
How I solved the issue:
It seems that doctrine:migrations:migrate locks itself in order to prevent multiple runs from the same command.
In order to bypass this, we can run the command inside a different process at each loop.
protected function execute(InputInterface $input, OutputInterface $output): int
{
$companies = $this->companyRepository->findAll();
foreach ($companies as $company){
$db = $company->getDatabaseUuid();
$connection = $this->tenantEntityManager->getConnection();
$connection->selectDatabase($db);
$process = new Process([
"bin/console",
"doctrine:migrations:migrate",
"--configuration=config/migrations/tenant_migrations.yaml",
"--em=tenant",
]);
$process->run();
if (!$process->isSuccessful()) {
throw new ProcessFailedException($process);
}
echo $process->getOutput();
}
return Command::SUCCESS;
}
This will lead to a second problem.
Since we run $connection->selectDatabase($db); in the current command process, but doctrine:migrations:migrate inside a new process, the entity manager won't have the database selected.
The solution was to create 2 commands as follows:
migrate:single-tenant command that can recieve a --db parameter and runs doctrine:migrations:migrate the default way
migrate:tenants command that get all the databases and run migrate:single-tenant --db=$db as a new process in a loop
This way we set the database on the same process as the execution of doctrine:migrations:migrate and in the same time we keep only one migration per process
Here are the final commands
migrate:single-tenant
protected function configure(): void
{
$this
->addOption('db', null, InputOption::VALUE_REQUIRED, 'Database selection')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$db = $input->getOption('db');
if (!$db) {
$io->error('"--db" option missing');
return Command::FAILURE;
}
$connection = $this->tenantEntityManager->getConnection();
$connection->selectDatabase($db);
$this->getApplication()
->find('doctrine:migrations:migrate')
->run(new ArrayInput([
'--configuration' => 'config/migrations/tenant_migrations.yaml',
'--em' => 'tenant'
]),$output);
$io->success("$db migrated succesfully");
return Command::SUCCESS;
}
migrate:tenants
protected function execute(InputInterface $input, OutputInterface $output): int
{
$companies = $this->companyRepository->findAll();
foreach ($companies as $company){
$db = $company->getDatabaseUuid();
$process = new Process([
"bin/console",
"migrate:single-tenant",
"--db=$db",
]);
$process->run();
if (!$process->isSuccessful()) {
throw new ProcessFailedException($process);
}
echo $process->getOutput();
}
return Command::SUCCESS;
}
I'm trying to get a simple "200 Response" test to work for a part of a website requiring an authenticated user. I think I've got the creation of the Session working, as during debugging the Controller function is called and a User is retrieved (using $this->getUser()).
However, afterwards the function fails with the following message:
1) App\Tests\Controller\SecretControllerTest::testIndex200Response
expected other status code for 'http://localhost/secret_url/':
error:
Multiple non-persisted new entities were found through the given association graph:
* A new entity was found through the relationship 'App\Entity\User#role' that was not configured to cascade persist operations for entity: ROLE_FOR_USER. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade
persist this association in the mapping for example #ManyToOne(..,cascade={"persist"}).
* A new entity was found through the relationship 'App\Entity\User#secret_property' that was not configured to cascade persist operations for entity: test123. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade pe
rsist this association in the mapping for example #ManyToOne(..,cascade={"persist"}). (500 Internal Server Error)
Failed asserting that 500 matches expected 200.
This would make sense if this was not already stored in the (MySQL) database and retrieved with Doctrine. The records are created using Fixtures on each run/for each test. This is why in the Controller $this->getUser() functions as expected.
The test I'm wanting to work:
public function testIndex200Response(): void
{
$client = $this->getAuthenticatedSecretUserClient();
$this->checkPageLoadResponse($client, 'http://localhost/secret_url/');
}
Get a user:
protected function getAuthenticatedSecretUserClient(): HttpKernelBrowser
{
$this->loadFixtures(
[
RoleFixture::class,
SecretUserFixture::class,
]
);
/** #var User $user */
$user = $this->entityManager->getRepository(User::class)->findOneBy(['username' => 'secret_user']);
$client = self::createClient(
[],
[
'PHP_AUTH_USER' => $user->getUsername(),
'PHP_AUTH_PW' => $user->getPlainPassword(),
]
);
$this->createClientSession($user, $client);
return $client;
}
Create a session:
// Based on https://symfony.com/doc/current/testing/http_authentication.html#using-a-faster-authentication-mechanism-only-for-tests
protected function createClientSession(User $user, HttpKernelBrowser $client): void
{
$authenticatedGuardToken = new PostAuthenticationGuardToken($user, 'chain_provider', $user->getRoles());
$tokenStorage = new TokenStorage();
$tokenStorage->setToken($authenticatedGuardToken);
$session = self::$container->get('session');
$session->set('_security_<security_context>', serialize($authenticatedGuardToken));
$session->save();
$cookie = new Cookie($session->getName(), $session->getId());
$client->getCookieJar()->set($cookie);
self::$container->set('security.token_storage', $tokenStorage);
}
This works for the creating of the client, session and cookie.
When the Request is executed to the $url in the first function, it gets into the endpoint, confirming the User is indeed authenticated.
According to the documentation here a User should be "refreshed" from via the configured provider (using Doctrine in this case) to check if a given object matches a stored object.
[..] At the beginning of the next request, it's deserialized and then passed to your user provider to "refresh" it (e.g. Doctrine queries for a fresh user).
I would expect this would also ensure that the session User is replaced with a Doctrine managed User object to prevent the error above.
How can I go about solving that the User in the session becomes a managed User during PhpUnit testing?
(Note: the production code works without any issue, this problem only arises during testing (legacy code now starting to get tests))
Ok, had multiple issues, but got it working doing the following:
First, was creating a Client using incorrect password, I was creating (in Fixtures) User entities with username and password being identical. The function getPlainPassword, though present in an interface, was not something stored, so was a blank value.
Corrected code:
$client = self::createClient(
[],
[
'PHP_AUTH_USER' => $user->getUsername(),
'PHP_AUTH_PW' => $user->getUsername(),
]
);
Next, a User not being refreshed took some more.
In config/packages/security.yaml, add the following:
security:
firewalls:
test:
security: ~
This is to create the "test" key, as creating that immediately in the next file will cause a permission denied error. In config/packages/test/security.yaml, create the following:
security:
providers:
test_user_provider:
id: App\Tests\Functional\Security\UserProvider
firewalls:
test:
http_basic:
provider: test_user_provider
This adds a custom UserProvider specifically for testing purposes (hence usage App\Tests\ namespace). You must register this service in your config/services_test.yaml:
services:
App\Tests\Functional\Security\:
resource: '../tests/Functional/Security'
Not sure you'll need it, but I added in config/packages/test/routing.yaml the following:
parameters:
protocol: http
As PhpUnit is testing via CLI, there by default is no secure connection, can vary by environment so see if you need it.
Lastly, config for test framework in config/packages/test/framework.yaml:
framework:
test: true
session:
storage_id: session.storage.mock_file
All of the above config (apart from the http bit) is to ensure that a custom UserProvider will be used to provider User objects during testing.
This might excessive for others, but our setup (legacy) has some custom work for providing Users for authentication (which seems very related but far out of my current issue scope).
Back on to the UserProvider, it's setup like so:
namespace App\Tests\Functional\Security;
use App\Entity\User;
use App\Repository\UserRepository;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
class UserProvider implements UserProviderInterface
{
/** #var UserRepository */
private $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function loadUserByUsername($username)
{
try {
return $this->userRepository->getByUsername($username);
} catch (UserNotFoundException $e) {
throw new UsernameNotFoundException("Username: $username unknown");
}
}
public function refreshUser(UserInterface $user)
{
return $this->loadUserByUsername($user->getUsername());
}
public function supportsClass($class)
{
return User::class === $class;
}
}
Note: should you use this, you need to have a getByUsername function in your UserRepository.
Please note, this might not be the solution for you. Maybe you need to change it up, maybe it's completely off. Either way, thought to leave a solution for any future souls.
I asked about using multiple entity manager for FOSUserBundle before, and it turns out that FOSUserBundle has (partially) supported that. All I need to do is to specify the connection / manager I want to use in the model_manager_name parameter, as explained here
fos_user:
# ........
model_manager_name: account1
Sample app/config/config.yml
With this FOSUserBundle will use account1 connection, and use user information in that connection's database.
doctrine:
dbal:
default_connection: default
connections:
account2:
dbname: account2
user: account2
password: password2
driver: pdo_mysql
host: localhost
port: ~
charset: UTF8
account1:
dbname: account1
user: account1
password: password1
driver: pdo_mysql
host: localhost
port: ~
charset: UTF8
default:
dbname: account
user: account
password: password
driver: pdo_mysql
host: localhost
port: ~
charset: UTF8
My app require that when a user goes to (for example) http://myapp.com/a/account1, the app will use account1 connection, and going to http://myapp.com/a/account2 will use account2's connection. For my application's logic, this is easily done from my controllers as I could use something like the following;
$em = $this->get('doctrine')->getManager('account2');
$repository = $this->get('doctrine')->getRepository($class, 'account2')
For the login part though, it's not that easy. FOSUserBundle runs as a service container, and I don't know where/how to dynamically change the model_manager_name's value. I do know though that in FOS\UserBundle\DependencyInjection\FOSUserExtension I can manually change its value the following way;
class FOSUserExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$processor = new Processor();
$configuration = new Configuration();
$config = $processor->processConfiguration($configuration, $configs);
$config['model_manager_name'] = 'account2';
// .................
Any thoughts?
The configuration is stored inside the container, in the fos_user.model_manager_name to be exact.
You can write a compiler pass. This will be executed just before freezing the container, it is the last place where you can change the container and it is the place to change the container based on other services.
Your compiler pass will look like this:
// src/Acme/DemoBundle/DependencyInjection/Compiler/ChangeModelManagerPass.php
namespace Acme\DemoBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class ChangeModelManagerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$request = $container->get('request');
$uri = $request->getUri();
// check if the uri matches some pattern which will cause a change in the
// `model_manager_name` setting
if (...) {
// ... do some stuff to get the correct model manager name
// set the setting
$container->setParameter('fos_user.model_manager_name', ...);
}
}
}
Read more about compiler passes in the docs or in this great blog post by Richard Miller.
In migration class depending on logic, I need to use different types of database connections. How in migration class to get new connection by connection name?
Currently in doctrine.yaml file I have connection names "default", "user", "admin" and "cron".
My migration class:
final class Version20190711123152 extends AbstractMigration
{
public function up(Schema $schema) : void
{
...
if($someCondition) {
$this->setConnection($wantedConnection) // how to set $wantedConnection for example on "admin" connection
}
}
/**
* #param Connection $connection
*/
public function setConnection(Connection $connection): void
{
$this->connection = $connection;
}
I am using Symfony 4.3
I don't know your exact use case, but I don't think the migrations should be conditional, i.e. you may end up with inconsistent databases across different environments.
Maybe consider storing migration files in separate directories and use different configuration and entity manager when running migrations.
# /config/migrations/default.yaml
name: "Default Migrations"
migrations_namespace: "App\Migrations\Default"
table_name: "doctrine_migration_versions"
migrations_directory: "src/Migrations/Default"
# /config/migrations/admin.yaml
name: "Admin Migrations"
migrations_namespace: "App\Migrations\Admin"
table_name: "doctrine_migration_versions"
migrations_directory: "src/Migrations/Admin"
Assuming you have configured two entity managers - one default and one i.e. with "admin" name you can run those migrations separately:
php bin/console doctrine:migrations:migrate --configuration=config/migrations/default.yaml
php bin/console doctrine:migrations:migrate --configuration=config/migrations/admin.yaml --em=admin