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

Designing (Testing) the Create Battle Endpoint

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

In the controller test directory, create a new class: BattleControllerTest. Make it extend the fancy ApiTestCase we've been working on:

... lines 1 - 2
namespace Tests\AppBundle\Controller\Api;
use AppBundle\Test\ApiTestCase;
class BattleControllerTest extends ApiTestCase
{
... lines 9 - 38
}

Start with public function testPOSTCreateBattle():

... lines 1 - 6
class BattleControllerTest extends ApiTestCase
{
... lines 9 - 15
public function testPOSTCreateBattle()
{
... lines 18 - 37
}
}

Go steal some code from ProgrammerControllerTest(). Copy the setup method that creates a user so that we can send the Authorization header:

... lines 1 - 6
class BattleControllerTest extends ApiTestCase
{
protected function setUp()
{
parent::setUp();
$this->createUser('weaverryan');
}
... lines 15 - 38
}

Create a Project & Programmer

For this endpoint, there are only two pieces of information we need to send: which programmer and which project will battle. So before we start, we need to create these. Add $this->createProject() and give it a name: my_project - that doesn't matter:

... lines 1 - 6
class BattleControllerTest extends ApiTestCase
{
... lines 9 - 15
public function testPOSTCreateBattle()
{
$project = $this->createProject('my_project');
... lines 19 - 37
}
}

If you open this method, this method simply creates a new Project and flushes it to the database:

... lines 1 - 21
class ApiTestCase extends KernelTestCase
{
... lines 24 - 322
/**
* @param string $name
* @return Project
*/
protected function createProject($name)
{
$project = new Project();
$project->setName($name);
$project->setDifficultyLevel(rand(1, 10));
$this->getEntityManager()->persist($project);
$this->getEntityManager()->flush();
return $project;
}
... lines 338 - 371
}

Next, create the programmer: $this->createProgrammer(). This takes an array of information about that programmer. Hmm, let's call him Fred. Pass weaverryan as the second argument: that will be the user who owns Fred:

... lines 1 - 6
class BattleControllerTest extends ApiTestCase
{
... lines 9 - 15
public function testPOSTCreateBattle()
{
$project = $this->createProject('my_project');
$programmer = $this->createProgrammer([
'nickname' => 'Fred'
], 'weaverryan');
... lines 22 - 37
}
}

Eventually, we'll need to restrict this endpoint so that we can only start battles with our programmers.

Next, send a POST request! In ProgrammerControllerTest, it was easy: we sent 3 scalar fields:

... lines 1 - 6
class ProgrammerControllerTest extends ApiTestCase
{
... lines 9 - 15
public function testPOSTProgrammerWorks()
{
$data = array(
'nickname' => 'ObjectOrienter',
'avatarNumber' => 5,
'tagLine' => 'a test dev!'
);
... lines 23 - 35
}
... lines 37 - 288
}

But now, it's a little different: we want to send data that identifies the related programmer and project resources. Should we send the id? Or the programmer's nickname?

Well, like normal with API's... it doesn't matter. But sending the id's makes sense. Create a new $data array with two fields: project set to $project->getId() and programmer set to $programmer->getId():

... lines 1 - 6
class BattleControllerTest extends ApiTestCase
{
... lines 9 - 15
public function testPOSTCreateBattle()
{
$project = $this->createProject('my_project');
$programmer = $this->createProgrammer([
'nickname' => 'Fred'
], 'weaverryan');
$data = array(
'project' => $project->getId(),
'programmer' => $programmer->getId()
);
... lines 27 - 37
}
}

I'm calling the keys programmer and project for obvious reasons: but they could be anything. We are in charge of naming the fields whatever we want. Just be sane and consistent: please don't call the fields bob and larry - everyone will hate you.

Finally, make the request: $response = $this->client->post(). For programmers, the URL is /api/programmers. Stay consistent with /api/battles. Pass an array as the second argument with a body key set to json_encode($data) and a headers key set to $this->getAuthorizedHeaders('weaverryan'):

... lines 1 - 6
class BattleControllerTest extends ApiTestCase
{
... lines 9 - 15
public function testPOSTCreateBattle()
{
$project = $this->createProject('my_project');
$programmer = $this->createProgrammer([
'nickname' => 'Fred'
], 'weaverryan');
$data = array(
'project' => $project->getId(),
'programmer' => $programmer->getId()
);
$response = $this->client->post('/api/battles', [
'body' => json_encode($data),
'headers' => $this->getAuthorizedHeaders('weaverryan')
]);
... lines 32 - 37
}
}

That will send a valid JSON web token for the weaverryan user - we created that in the previous course.

Asserts!

Whew, okay. That's it. So even though Battle is dependent on two other resources, it works pretty much the same. Add some basic asserts: $this->assertEquals() that 201 will be the response status code:

... lines 1 - 6
class BattleControllerTest extends ApiTestCase
{
... lines 9 - 15
public function testPOSTCreateBattle()
{
$project = $this->createProject('my_project');
$programmer = $this->createProgrammer([
'nickname' => 'Fred'
], 'weaverryan');
$data = array(
'project' => $project->getId(),
'programmer' => $programmer->getId()
);
$response = $this->client->post('/api/battles', [
'body' => json_encode($data),
'headers' => $this->getAuthorizedHeaders('weaverryan')
]);
$this->assertEquals(201, $response->getStatusCode());
... lines 34 - 37
}
}

In Battle, one of the fields that should be returned is didProgrammerWin. Make sure that exists with $this->asserter()->assertResponsePropertyExists() and look for didProgrammerWin:

... lines 1 - 6
class BattleControllerTest extends ApiTestCase
{
... lines 9 - 15
public function testPOSTCreateBattle()
{
$project = $this->createProject('my_project');
$programmer = $this->createProgrammer([
'nickname' => 'Fred'
], 'weaverryan');
$data = array(
'project' => $project->getId(),
'programmer' => $programmer->getId()
);
$response = $this->client->post('/api/battles', [
'body' => json_encode($data),
'headers' => $this->getAuthorizedHeaders('weaverryan')
]);
$this->assertEquals(201, $response->getStatusCode());
$this->asserter()
->assertResponsePropertyExists($response, 'didProgrammerWin');
// todo for later
//$this->assertTrue($response->hasHeader('Location'));
}
}

I'll also add a todo to check for the Location header later. Remember, when you create a resource, you're supposed to return a Location header to the URL where the client can view the new resource. We don't have a GET endpoint for a battle yet, so we'll skip this.

The hard work is done: we've designed the new endpoint. Let's bring it to life!

Leave a comment!

0
Login or Register to join the conversation
Cat in space

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

This tutorial uses an older version of Symfony. The concepts of Hypermedia & HATEOAS are still valid. But I recommend using API Platform in modern Symfony apps.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.0.*", // v3.0.3
        "doctrine/orm": "^2.5", // v2.5.4
        "doctrine/doctrine-bundle": "^1.6", // 1.6.2
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // v2.10.0
        "sensio/distribution-bundle": "^5.0", // v5.0.4
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.14
        "incenteev/composer-parameter-handler": "~2.0", // v2.1.2
        "jms/serializer-bundle": "^1.1.0", // 1.1.0
        "white-october/pagerfanta-bundle": "^1.0", // v1.0.5
        "lexik/jwt-authentication-bundle": "^1.4", // v1.4.3
        "willdurand/hateoas-bundle": "^1.1" // 1.1.1
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.6
        "symfony/phpunit-bridge": "^3.0", // v3.0.3
        "behat/behat": "~3.1@dev", // dev-master
        "behat/mink-extension": "~2.2.0", // v2.2
        "behat/mink-goutte-driver": "~1.2.0", // v1.2.1
        "behat/mink-selenium2-driver": "~1.3.0", // v1.3.1
        "phpunit/phpunit": "~4.6.0", // 4.6.10
        "doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
    }
}
userVoice