Buy

Test Fixtures and the PropertyAccess Component

Howdy big error! Now that I can see you, I can fix you! Remember, back in ProgrammerController, we're always assuming there's a weaverryan user in the database:

98 lines src/AppBundle/Controller/Api/ProgrammerController.php
... lines 1 - 19
public function newAction(Request $request)
{
... lines 22 - 25
$form->submit($data);
$programmer->setUser($this->findUserByUsername('weaverryan'));
$em = $this->getDoctrine()->getManager();
$em->persist($programmer);
$em->flush();
... lines 33 - 42
}
... lines 44 - 98

We'll fix this later with some proper authentication, but for now, when we run our tests, we need to make sure that user is cozy and snug in the database.

Creating a test User

Create a new protected function called createUser() with a required username argument and one for plainPassword. Make that one optional: in this case, we don't care what the user's password will be:

I'll paste in some code for this: it's pretty easy stuff. I'll trigger autocomplete on the User class to get PhpStorm to add that use statement for me. This creates the User and gives it the required data. The getService() function we created lets us fetch the password encoder out so we can use it, what a wonderfully trained function:

259 lines src/AppBundle/Test/ApiTestCase.php
... lines 1 - 212
protected function createUser($username, $plainPassword = 'foo')
{
$user = new User();
$user->setUsername($username);
$user->setEmail($username.'@foo.com');
$password = $this->getService('security.password_encoder')
->encodePassword($user, $plainPassword);
$user->setPassword($password);
... lines 221 - 226
}
... lines 228 - 259

Let's save this! Since we'll need the EntityManager a lot in this class, let's add a protected function getEntityManager(). Use getService() with doctrine.orm.entity_manager. And since I love autocomplete, give this PHPDoc:

235 lines src/AppBundle/Test/ApiTestCase.php
... lines 1 - 226
/**
* @return EntityManager
*/
protected function getEntityManager()
{
return $this->getService('doctrine.orm.entity_manager');
}
... lines 234 - 235

Now $this->getEntityManager()->persist() and $this->getEntityManager()->flush(). And just in case whoever calls this needs the User, let's return it.

235 lines src/AppBundle/Test/ApiTestCase.php
... lines 1 - 210
protected function createUser($username, $plainPassword = 'foo')
{
... lines 213 - 219
$em = $this->getEntityManager();
$em->persist($user);
$em->flush();
return $user;
}
... lines 226 - 235

We could just go to the top of testPOST and call this there. But really, our entire system is kind of dependent on this user. So to truly fix this, let's put it in setup(). Don't forget to call parent::setup() - we've got some awesome code there. Then, $this->createUser('weaverryan'):

36 lines src/AppBundle/Tests/Controller/Api/ProgrammerControllerTest.php
... lines 1 - 5
class ProgrammerControllerTest extends ApiTestCase
{
protected function setUp()
{
parent::setUp();
$this->createUser('weaverryan');
}
... lines 14 - 34
}

I'd say we've earned a greener test - let's try it!

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

Yay!

Testing GET one Programmer

Now, let's test the GET programmer endpoint:

54 lines src/AppBundle/Tests/Controller/Api/ProgrammerControllerTest.php
... lines 1 - 5
class ProgrammerControllerTest extends ApiTestCase
{
... lines 8 - 35
public function testGETProgrammer()
{
... lines 38 - 51
}
}

Hmm, so we have another data problem: before we make a request to fetch a single programmer, we need to make sure there's one in the database.

To do that, call out to an imaginary function createProgrammer() that we'll write in a second. This will let us pass in an array of whatever fields we want to set on that Programmer:

54 lines src/AppBundle/Tests/Controller/Api/ProgrammerControllerTest.php
... lines 1 - 35
public function testGETProgrammer()
{
$this->createProgrammer(array(
'nickname' => 'UnitTester',
'avatarNumber' => 3,
));
... lines 42 - 51
}
... lines 53 - 54

The Programmer class has a few other fields and the idea is that if we don't pass something here, createProgrammer() will invent some clever default for us.

Let's get to work in ApiTestCase: protected function createProgrammer() with an array of $data as the argument. And as promised, our first job is to use array_merge() to pass in some default values. One is the powerLevel - it's required - and if it's not set, give it a random value from 0 to 10. Next, create the Programmer:

258 lines src/AppBundle/Test/ApiTestCase.php
... lines 1 - 228
protected function createProgrammer(array $data)
{
$data = array_merge(array(
'powerLevel' => rand(0, 10),
... lines 233 - 235
), $data);
... lines 237 - 238
$programmer = new Programmer();
... lines 240 - 247
}
... lines 249 - 258

Ok, maybe you're expecting me to iterate over the data, put the string set before each property name, and call that method. But no! There's a better way.

Getting down with PropertyAccess

Create an $accessor variable that's set to ProperyAccess::createPropertyAccessor(). Hello Symfony's PropertyAccess component! Now iterate over data. And instead of the "set" idea, call $accessor->setValue(), pass in $programmer, passing $key - which is the property name - and pass in the $value we want to set:

258 lines src/AppBundle/Test/ApiTestCase.php
... lines 1 - 228
protected function createProgrammer(array $data)
{
... lines 231 - 237
$accessor = PropertyAccess::createPropertyAccessor();
$programmer = new Programmer();
foreach ($data as $key => $value) {
$accessor->setValue($programmer, $key, $value);
}
... lines 243 - 247
}
... lines 249 - 258

The PropertyAccess component is what works behind the scenes with Symfony's Form component. So, it's great at calling getters and setters, but it also has some really cool superpowers that we'll need soon.

The Programmer has all the data it needs, except for this $user relationship property. To set that, we can just add user to the defaults and query for one. I'll paste in a few lines here: I already setup our UserRepository to have a findAny() method on it:

258 lines src/AppBundle/Test/ApiTestCase.php
... lines 1 - 228
protected function createProgrammer(array $data)
{
$data = array_merge(array(
'powerLevel' => rand(0, 10),
'user' => $this->getEntityManager()
->getRepository('AppBundle:User')
->findAny()
), $data);
... lines 237 - 247
}
... lines 249 - 258

And finally, the easy stuff! Persist and flush that Programmer. And return it too for good measure:

258 lines src/AppBundle/Test/ApiTestCase.php
... lines 1 - 228
protected function createProgrammer(array $data)
{
... lines 231 - 242
$this->getEntityManager()->persist($programmer);
$this->getEntityManager()->flush();
return $programmer;
}
... lines 249 - 258

Finishing the GET Test

Phew! With that work done, finishing the test is easy. Make a GET request to /api/programmers/UnitTester. And as always, we want to start by asserting the status code:

54 lines src/AppBundle/Tests/Controller/Api/ProgrammerControllerTest.php
... lines 1 - 35
public function testGETProgrammer()
{
$this->createProgrammer(array(
'nickname' => 'UnitTester',
'avatarNumber' => 3,
));
$response = $this->client->get('/api/programmers/UnitTester');
$this->assertEquals(200, $response->getStatusCode());
... lines 45 - 51
}
... lines 53 - 54

I want to assert that we get the properties we expect. If you look in ProgrammerController, we're serializing 4 properties: nickname, avatarNumber, powerLevel and tagLine. To avoid humiliation let's assert that those actually exist.

I'll use an assertEquals() and put those property names as the first argument in a moment. For the second argument - the actual value - we can use array_keys() on the json decoded response body - which I'll cleverly call $data. Guzzle can decode the JSON for us if we call $response->json(). This gives us the decoded JSON and array_keys gives us the field names in it. Back in the first argument to assertEquals(), we'll fill in the fields: nickname, avatarNumber, powerLevel and tagLine - even if it's empty:

54 lines src/AppBundle/Tests/Controller/Api/ProgrammerControllerTest.php
... lines 1 - 35
public function testGETProgrammer()
{
... lines 38 - 42
$response = $this->client->get('/api/programmers/UnitTester');
$this->assertEquals(200, $response->getStatusCode());
$data = $response->json();
$this->assertEquals(array(
'nickname',
'avatarNumber',
'powerLevel',
'tagLine'
), array_keys($data));
}
... lines 53 - 54

Ok, time to test-drive this:

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

Great success! Now let's zero in and make our assertions a whole lot more ...assertive :)

Leave a comment!

  • 2016-10-16 Johan

    I notice that there are so many different ways of tackling this haha

    For now I just fixed it by adding the CASCADEs. The solution with the sqlite database sounds interesting though. I might try that whenever simple CASCADEs are not possible anymore and the purger breaks :)

    Thank you for your time!

  • 2016-10-16 weaverryan

    Hey Johan!

    So I think I have had similar situations where I've hit the same problems and drawn the same conclusions as you! The purger actually calculates the correct delete order to avoid foreign key problems, but sometimes due to circular relationships, it's just not possible. In fact, I just hit the yesterday, and didn't bother debugging it - I just added the CASCADEs (it was a safe enough situation for me to do this). And yes, I've also done the foreign_key_checks thing too :p.

    Btw, there is one other interesting solution for testing, which I know others have used, but I haven't ever quite tried: that is to prepare an sqlite database with a known data set (or perhaps, even empty), copy this to the correct location before the test to have it automatically used. Here are some details: http://stackoverflow.com/quest.... Don't use the "in memory" option - that only works if you're using Symfony's internal test client - whereas here we're making real HTTP requests in a different thread (this is my preferred way).

    Cheers and good luck!

  • 2016-10-15 Johan

    I was trying your suggestion using the purger, but this actually gave the error. I guess it tries to remove users before removing posts.

    I think it would be painful to manually clear tables before starting your test because you need to know and specify the order in which you delete the tables.

    In this case I think setting the onDelete to CASCADE would solve my problem, but only if all my FKs have this option, which probably won't be the case. I'm literally trying to clear the entire database.

    I'm thinking of just writing a PHP script that temporarily turns off foreign key constraints ("SET foreign_key_checks = 0;"), iterate over all tables and DELETE all rows.

    Thank you for your reply, very much appreciated!

  • 2016-10-15 weaverryan

    Hey Johan!

    Yea, this is a classic problem :). So, by default in Doctrine, when Doctrine setups of your relationship in the database, it doesn't add *any* "ON DELETE" behavior. This means that if you try to delete a row in a table, but there are other records that reference this as a foreign key, it'll fail. And this is a good default, because it's safe. So, you have 2 options to fix this:

    1) You can fix it in your test. What I mean is, you can make sure that you empty the posts table *before* your test starts (so that it is able to delete a user later). Sometimes, I will literally - in my setup() method of my test - empty a few tables manually, with code like this:


    // get the entity manager, however you do in your test

    $em
    ->createQuery('DELETE FROM AppBundle:Post')
    ->execute()
    ;

    OR, you could empty *every* table in your project. We actually do that in this tutorial. If you look in the ApiTestCase class that I setup for the tutorial, in the setup() method, we call a purgeDatabase() method, which does the following:


    // $em is the entity manager
    $purger = new ORMPurger($em);
    $purger->purge();

    2) When it makes sense, an even better solution is to fix this in your application. What I mean is, perhaps it *is* ok in your app that if a user were somehow ever deleted, that all of that user's posts are also deleted (or maybe not deleted, but their user/owner" set to null. If you feel comfortable doing this, then you'll update your Post.user property to add a JoinColumn:


    // Post.php

    /**
    * @ORM\ManyToOne(targetEntity="User")
    * @ORM\JoinColumn(onDelete="CASCADE")
    */
    private $user;

    The other likely value instead of "CASCADE" would be "SET NULL". You'll need to generate a migration for this, since this is a change that affects your database.

    Let me know if that helps! This is a really tough issue that I also struggle with - the correct answer depends on your app. I typically try to completely empty my database before each test, but eventually that can slow your tests down. I usually tackle that problem later when/if that becomes an issue.

    Cheers!

  • 2016-10-15 Johan

    Hmm, I'm getting an error. I have a situation similar to this: a user has many posts. When it is trying to remove the user, it crashes because of foreign key constraints, which makes sense of course. How do I work around this? :(

  • 2016-08-25 Rakib Ahmed Shovon

    oh .. got it . done

  • 2016-08-24 Rakib Ahmed Shovon

    There was 1 failure:

    1) AppBundle\Tests\Controller\Api\ProgrammerControllerTest::testGETProgrammer
    Failed asserting that two arrays are equal.
    --- Expected
    +++ Actual
    @@ @@
    Array (
    - 0 => 'nickname'
    - 1 => 'avatarNumber'
    - 2 => 'powerLevel'
    - 3 => 'tagLine'
    + 0 => 'id'
    + 1 => 'nickname'
    + 2 => 'avatar_number'
    + 3 => 'power_level'
    + 4 => 'user'
    )

    C:\Users\rakib\Site\symfony2-rest\src\AppBundle\Tests\Controller\Api\ProgrammerControllerTest.php:59

    avatar_number become avatarNumber ...
    in Windows

  • 2016-05-09 Vincent Wong

    Inore my last comment I think I open the wrong ApiTestCase file, all good now. Thanks.

  • 2016-05-07 weaverryan

    Hey Vincent!

    Yep, this tutorial uses Guzzle version 5 - they're always releasing new versions on me! But, if you download the course code for course #4 (https://knpuniversity.com/scre... - you can check out the new version of the `ApiTestCase`. I upgraded to Symfony 3 and Guzzle 6 for that tutorial, and updated all that History stuff for the new version :).

    Cheers!

  • 2016-05-06 Vincent Wong

    Hi Ryan, which Guzzle version that you are using for this example? I tried using latest Guzzle version 6.2 and got some error in the History class. I notice that Guzzle make quite a bit of an update on the version 6.