Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Using a Test Database

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

We're using the built-in PHP web server running on port 8000. We have that hardcoded at the top of ApiTestCase: when the Client is created, it always goes to localhost:8000. Bummer! All of our fellow code battlers will need to have the exact same setup.

We need to make this configurable - create a new variable $baseUrl and set it to an environment variable called TEST_BASE_URL - I'm making that name up. Use this for the base_url option:

... lines 1 - 45
public static function setUpBeforeClass()
{
$baseUrl = getenv('TEST_BASE_URL');
self::$staticClient = new Client([
'base_url' => $baseUrl,
'defaults' => [
'exceptions' => false
]
]);
... lines 55 - 59
}
... lines 61 - 273

There are endless ways to set environment variables. But we want to at least give this a default value. Open up app/phpunit.xml.dist. Get rid of those comments - we want a php element with an env node inside. I'll paste that in:

... lines 1 - 3
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="bootstrap.php.cache"
>
... lines 10 - 17
<php>
<env name="TEST_BASE_URL" value="http://localhost:8000" />
</php>
... lines 21 - 34
</phpunit>

If you have our setup, everything just works. If not, you can set this environment variable or create a phpunit.xml file to override everything.

Let's double-check that this all works:

phpunit -c app --filter testGETProgrammersCollection src/AppBundle/Tests/Controller/Api/ProgrammerControllerTest.php

Tests Killed our Database

One little bummer is that the tests are using our development database. Since those create a weaverryan user with password foo, that still works. But the cute programmer we created earlier is gone - they've been wiped out, sent to /dev/null... hate to see that.

Configuring the test Environment

Symfony has a test environment for just this reason. So let's use it! Start by copying app_dev.php to app_test.php, then change the environment key from dev to test. To know if this all works, put a temporary die statement right on top:

31 lines web/app_test.php
<?php
die('working?');
... lines 3 - 24
$kernel = new AppKernel('test', true);
... lines 26 - 31

We'll setup our tests to hit this file instead of app_dev.php, which is being used now because Symfony's server:run command sets up the web server with that as the default.

Once we do that, we can setup the test environment to use a different database name. Open config.yml and copy the doctrine configuration. Paste it into config_test.yml to override the original. All we really want to change is dbname. I like to just take the real database name and suffix it with _test:

... lines 1 - 17
doctrine:
dbal:
dbname: "%database_name%_test"

Ok, last step. In phpunit.xml.dist, add a /app_test.php to the end of the URL. In theory, all our API requests will now hit this front controller.

Run the test! This shouldn't pass - it should hit that die statement on every endpoint:

phpunit -c app --filter testGETProgrammersCollection src/AppBundle/Tests/Controller/Api/ProgrammerControllerTest.php

They fail! But not for the reason we wanted:

Unknown database `symfony_rest_recording_test`

Woops, I forgot to create the new test database. Fix this with doctrine:database:create in the test environment and doctrine:schema:create:

php app/console doctrine:database:create --env=test
php app/console doctrine:schema:create --env=test

Try it again:

phpunit -c app --filter testGETProgrammersCollection src/AppBundle/Tests/Controller/Api/ProgrammerControllerTest.php

Huh, it passed. Not expected. We should be hitting this die statement. Something weird is going on.

Debugging Weird/Failing Requests

Go into ProgrammerControllerTest to debug this. We should be going to a URL with app_test.php at the front, but it seems like that's not happening. Use $this->printLastRequestUrl() after making the request:

... lines 1 - 53
public function testGETProgrammersCollection()
{
... lines 56 - 64
$response = $this->client->get('/api/programmers');
$this->printLastRequestUrl();
... lines 67 - 70
}
... lines 72 - 73

This is one of the helper functions I wrote - it shows the true URL that Guzzle is using.

Now run the test:

phpunit -c app --filter testGETProgrammersCollection src/AppBundle/Tests/Controller/Api/ProgrammerControllerTest.php

Huh, so there's not app_test.php in the URL. Ok, so here's the deal. With Guzzle, if you have this opening slash in the URL, it takes that string and puts it right after the domain part of your base_url. Anything after that gets run over. We could fix this by taking out the opening slash everywhere - like api/programmers - but I just don't like that: it looks weird.

Properly Prefixing all URIs

Instead, get rid of the app_test.php part in phpunit.xml.dist:

... lines 1 - 3
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
... lines 5 - 17
<php>
<env name="TEST_BASE_URL" value="http://localhost:8000" />
</php>
... lines 21 - 34
</phpunit>

We'll solve this a different way. When the Client is created in ApiTestCase, we have the chance to attach listeners to it. Basically, we can hook into different points, like right before a request is sent or right after. Actually, I'm already doing that to keep track of the Client's history for some debugging stuff.

I'll paste some code, and add a use statement for this BeforeEvent class:

... lines 1 - 10
use GuzzleHttp\Event\BeforeEvent;
... lines 12 - 20
class ApiTestCase extends KernelTestCase
{
... lines 23 - 46
public static function setUpBeforeClass()
{
... lines 49 - 59
// guaranteeing that /app_test.php is prefixed to all URLs
self::$staticClient->getEmitter()
->on('before', function(BeforeEvent $event) {
$path = $event->getRequest()->getPath();
if (strpos($path, '/api') === 0) {
$event->getRequest()->setPath('/app_test.php'.$path);
}
});
... lines 68 - 69
}
... lines 71 - 281
}

Ah Guzzle - you're so easy to understand sometimes! So as you can probably guess, this function is called before every request is made. All we do is look to see if the path starts with /api. If it does, prefix that with /app_test.php. This will make every request use that front controller, without ever needing to think about that in the tests.

Give it another shot:

phpunit -c app --filter testGETProgrammersCollection src/AppBundle/Tests/Controller/Api/ProgrammerControllerTest.php

Errors! Yes - it doesn't see a programmers property in the response because all we have is this crumby die statement text. Now that we know things hit app_test.php, go take that die statement out of it. And remove the printLastRequestUrl(). Run the entire test suite:

phpunit -c app

Almost! There's 1 failure! Inside testPOST - we're asserting that the Location header is this string, but now it has the app_test.php part in it. That's a false failure - our code is really working. Let's soften that test a bit. How about replacing assertEquals() with assertStringEndsWith(). Now let's see some passing:

phpunit -c app

Yay!

Leave a comment!

22
Login or Register to join the conversation
mehdi Avatar

Hello,

I have a weird error when adding the test database.
When creating a programmer in the ProgrammerControllerTest using the dev env., the programmer is created in test db not the base db, why ?

Reply

Hey mehdi

That's because the tests have a special setup, when you call self::bootKernel() on any test method, the kernel by default will choose "test" environment, you can change it by passing as first argument an array with the key "APP_ENV", or by setting up an environment variable

Cheers!

Reply
Vladimir Z. Avatar
Vladimir Z. Avatar Vladimir Z. | posted 4 years ago

How do you create an `app_test.php` in Symfony 4 for testing purposes?

Reply

Hey Vladimir Z.

In Symfony4 you only have one "app.php" file, and in reality, it's not called "app.php" anymore, instead it's called "index.php". So, you don't have to create another file for your testing environment, you only have to set the APP_ENV global variable to "test". So, if you are using PHPUnit, you will have to declare that variable in your phpunit.xml:


<php>
...
<env name="APP_ENV" value="test"/>
</php>

Cheers!

Reply
Vladimir Z. Avatar
Vladimir Z. Avatar Vladimir Z. | MolloKhan | posted 4 years ago | edited

MolloKhan if my PHPUnit test cases use Guzzle to issue requests to API endpoints, will it automatically recognize these Guzzle requests as coming from the PHPUnit tests?

1 Reply

Good question. I'm not totally sure about it, you can give it a try. If it doesn't work let me know, and probably you may want to use the "BrowserKit" from Symfony meanwhile we find a proper solution to this situation
https://symfony.com/doc/cur...

Cheers!

Reply
Vladimir Z. Avatar
Vladimir Z. Avatar Vladimir Z. | MolloKhan | posted 4 years ago | edited

MolloKhan I see that the Symfony Client has an 'enviroment' parameter that can be used (haven't tested it yet). I was wondering whether Guzzle has something like that. MolloKhan do you have an idea here?

Reply

Actually, there is nothing wrong using Symfony's client instead of Guzzle in your tests, it's easier to use and it does not require any setup

Reply
Shaun T. Avatar
Shaun T. Avatar Shaun T. | posted 4 years ago

Another Symfony 4 question!

In the documentation it says to add the following to phpunit.xml.dist:

<phpunit>
<php>

<env name="DATABASE_URL" value="mysql://USERNAME:PASSWORD@127.0.0.1/DB_NAME"/>
</php>

</phpunit>

However when I try to run

bin/console doctrine:database:create --env=test

I get an error saying that the database already exists - but it seems to be trying to recreate the dev database with settings from .env

Any idea what I am doing wrong here?

Reply

Hey Shaun T.

That config only works when working on PHPUnit, you need to specify your test database in your test doctrine.yaml file.


// config/packages/test/doctrine.yaml
doctrine:
dbal:
url: '%env(resolve:DATABASE_URL)%_test'

Cheers!

Reply
Shaun T. Avatar
Shaun T. Avatar Shaun T. | MolloKhan | posted 4 years ago | edited

Thanks MolloKhan :)

Reply
Shaun T. Avatar
Shaun T. Avatar Shaun T. | Shaun T. | posted 4 years ago | edited

Shaun T. I have come across a problem related to this.

In my TokenControllerTest I can create a user in the test database, but in TokenController findOneBy is querying the Development database!

Do you know how I can resolve this please?

Reply

Hmm, how are you executing that controller's action? You have to assure that your kernel is booting on test environment, like "PHPUnit KernelCase class" does when you run self::bootKernel();

Reply
Shaun T. Avatar
Shaun T. Avatar Shaun T. | MolloKhan | posted 4 years ago | edited

Thanks MolloKhan, you were right, it was the Kernel :)

1 Reply
Shaun T. Avatar
Shaun T. Avatar Shaun T. | posted 4 years ago

Hey guys, I'm using Symfony 4, could you give me some guidance on how I can setup phpunit to use a test Database using this version?

Reply

Hey Shaun,

Yes, we can! Actually, there's Symfony Demo project that's on Symfony 4 now and has PHPUnit configured, you can look at the code here: https://github.com/symfony/...

Cheers!

Reply
Shaun T. Avatar
Shaun T. Avatar Shaun T. | victor | posted 4 years ago | edited

Thanks victor :)

I have had a look at the Symfony demo and it seems to have it's own client rather than using Guzzle.

How can the setUpBeforeClass() method be modified so that it uses the Symfony client rather than Guzzle?

public static function setUpBeforeClass()
{
self::$staticClient = new Client([
'base_url' => 'http://localhost:8000',
'defaults' => [
'exceptions' => false
]
]);
self::$history = new History();
self::$staticClient->getEmitter()
->attach(self::$history);

self::bootKernel();
}

Reply

Hey Shaun,

If you want to use Symfony's client instead of Guzzle, just do as Symfony Demo does, it uses exactly Symfony client :) i.e. you just need to extend WebTestCase and then with static::createClient(); you will get the client.

Cheers!

Reply
Default user avatar
Default user avatar Roy Hochstenbach | posted 6 years ago

Setting the 'TEST_BASE_URL' value in phpunit.xml.dist doesn't seem to be working. getenv('TEST_BASE_URL') returns an empty response.

Reply
Default user avatar

Hey I just had a similar problem although the response was not empty Guzzle was kind of ignoring the "/app_test.php" part of the base url. The problem was that in my setup (Symfony 3, Guzzle ^6.2) Guzzle overrides the path if the request uses an absolute path like "/api/...". Using "api/..." instead solved the problem for me.

Since it worked in the video I suppose this might be an issue with the later versions.

Reply

You're absolutely correct about the absolute paths (and I *do* think this was something that was added in some more recent versions of Guzzle). To get around it, we use a middleware that adds the app_test.php even when the URL starts with a slash. It's an annoying little thing to need to add, but it works pretty well. Here's the Guzzle 6 version for those who are curious: https://gist.github.com/wea...

Cheers!

Reply

This just saved my day. :D Thank You

1 Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial uses an older version of Symfony. The concepts of REST are still valid, but I recommend using API Platform in new Symfony apps.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.3.3",
        "symfony/symfony": "2.6.*", // v2.6.11
        "doctrine/orm": "~2.2,>=2.2.3,<2.5", // v2.4.7
        "doctrine/dbal": "<2.5", // v2.4.4
        "doctrine/doctrine-bundle": "~1.2", // v1.4.0
        "twig/extensions": "~1.0", // v1.2.0
        "symfony/assetic-bundle": "~2.3", // v2.6.1
        "symfony/swiftmailer-bundle": "~2.3", // v2.3.8
        "symfony/monolog-bundle": "~2.4", // v2.7.1
        "sensio/distribution-bundle": "~3.0,>=3.0.12", // v3.0.21
        "sensio/framework-extra-bundle": "~3.0,>=3.0.2", // v3.0.7
        "incenteev/composer-parameter-handler": "~2.0", // v2.1.0
        "hautelook/alice-bundle": "0.2.*", // 0.2
        "jms/serializer-bundle": "0.13.*" // 0.13.0
    },
    "require-dev": {
        "sensio/generator-bundle": "~2.3", // v2.5.3
        "behat/behat": "~3.0", // v3.0.15
        "behat/mink-extension": "~2.0.1", // v2.0.1
        "behat/mink-goutte-driver": "~1.1.0", // v1.1.0
        "behat/mink-selenium2-driver": "~1.2.0", // v1.2.0
        "phpunit/phpunit": "~4.6.0" // 4.6.4
    }
}