Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This course is archived!
This tutorial uses a deprecated micro-framework called Silex. The fundamentals of REST are still ?valid, but the code we use can't be used in a real application.

New Battle Resource (the Scenario)

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 $12.00

Let's login to the site. Don't forget, our tests like to mess with our database, so I'm going to delete the SQLite database file and it'll regenerate with some nice test data:

rm data/code_battles.sqlite

We'll login as ryan@knplabs.com password foo.

We already know that I'm able to create a programmer. And we even have some really nice API endpoints for this. The other part of the site is all about battles. If I click "Start Battle", this is a list of projects that are in the database right now. If I dare to select one of those project, it starts an EPIC CODE Battle OF HISTORY between the programmer and the project and picks a winner.

A battle is another type of resource, but it can't be created yet in the API. Let's fix that!

New Battle Feature

Like my other resources, I already have a class that models this. You can see there's a programmer, a project, the outcome didProgrammerWin and it even stores the date it was fought and some extra notes:

<?php
namespace KnpU\CodeBattle\Model;
class Battle
{
/* All public properties are persisted */
public $id;
/**
* @var Programmer
*/
public $programmer;
/**
* @var Project
*/
public $project;
public $didProgrammerWin;
public $foughtAt;
public $notes;
}

Let's make the endpoint to create new battles. We're going to start like always by creating a new feature - battle.feature. The API clients are going to want to create battles to see if their programmers can take on and defeat these projects. After the business value, the next line is the person that's benefiting from the new feature and finally we have a little description:

Feature:
In order to prove my programmers' worth against projects
As an API client
I need to be able to create and view battles
... lines 5 - 25

Create Battle Scenario

Let's add the first Scenario: Creating a new Battle. If we go back to programmer.feature, we can copy a lot of this. First, in order to create a battle, we're probably going to need to be authenticated. So, I'll copy this background:

Feature:
In order to prove my programmers' worth against projects
... lines 3 - 5
Background:
Given the user "weaverryan" exists
And "weaverryan" has an authentication token "ABCD123"
And I set the "Authorization" header to be "token ABCD123"
... line 10
Scenario: Create a battle
... lines 12 - 25

I'm going to go back and copy the entire scenario for creating a programmer. After all, this is an API, so creating a resource should always look pretty much the same.

Let's work from the end backwards and think about how we want the response to look. We know there's going to be a Location header, because there's always a Location header after creating a resource. But we don't know what URL that's going to be yet, because we don't have an endpoint yet for viewing a single battle. So we'll just say that the Location header should exist. And if you look at the Battle class, you'll see there's a didProgrammerWin property. Let's just make sure that exists as well - we don't know if it's going to be true or false, because there's some randomness. Let's update the URL to /api/battles and the status code of 201 looks perfect:

... lines 1 - 10
Scenario: Create a battle
... lines 12 - 20
When I request "POST /api/battles"
Then the response status code should be 201
And the "Location" header should exist
And the "didProgrammerWin" property should exist

Creating a Programmer and Project First

In order to create a Battle - we'll need to send a programmer and a project. And probably the way we'll want the client to do that is by sending the programmer and the project's ids. So let's send programmerId and projectId - but we don't know yet what these should be set to.

Next, in order for us to start a battle, there needs to already be a programmer and a project sitting in the database. So before this line, we'll need to say Given there is a programmer called, and we'll create a new programmer called Fred. Again, these are all built-in Behat definitions that I created before we started working and they all live in either ApiFeatureContext or ProjectContext. If you want to know what I'm doing behind the scenes, just open up those classes. There's another one for And there is a project called, and we'll say "my_project":

... lines 1 - 10
Scenario: Create a battle
Given there is a project called "my_project"
And there is a programmer called "Fred"
... lines 14 - 25

A little problem still exists: we don't know what the id's are of the programmer and project we just created. So I don't know what to put in the request body - we really want whatever ids those new things have. This is a really difficult problem with testing API's. So one of the things I've put into my testing system already, is the ability to do things like this:

... lines 1 - 10
Scenario: Create a battle
Given there is a project called "my_project"
And there is a programmer called "Fred"
And I have the payload:
"""
{
"programmerId": "%programmers.Fred.id%",
"projectId": "%projects.my_project.id%"
}
"""
... lines 21 - 25

It's a special syntax. And what this will do is go find a programmer whose nickname is "Fred" and give us its id. It'll create a query for that dynamically. This syntax is totally special - it's not something built into Behat. If you want to know how it works, open ApiFeatureContext, scroll all the way to the bottom and find the processReplacements() function. It parses out that "%" syntax, looks for these wildcards, and lets us do some of that magic. This will be really handy, and we'll use it a few more times.

We'll do the same thing for projects. This looks great!

... lines 1 - 10
Scenario: Create a battle
Given there is a project called "my_project"
And there is a programmer called "Fred"
And I have the payload:
"""
{
"programmerId": "%programmers.Fred.id%",
"projectId": "%projects.my_project.id%"
}
"""
When I request "POST /api/battles"
Then the response status code should be 201
And the "Location" header should exist
And the "didProgrammerWin" property should exist

You know I like watching my tests fail first, so let's try it out. We'll just run this new battle.feature file:

php vendor/bin/behat features/api/battle.feature

Instead of 201, we get the 404 because the endpoint doesn't exist. That's awesome. Now let's make this work!

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 a deprecated micro-framework called Silex. The fundamentals of REST are still ?valid, but the code we use can't be used in a real application.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "silex/silex": "~1.0", // v1.3.2
        "symfony/twig-bridge": "~2.1", // v2.7.3
        "symfony/security": "~2.4", // v2.7.3
        "doctrine/dbal": "^2.5.4", // v2.5.4
        "monolog/monolog": "~1.7.0", // 1.7.0
        "symfony/validator": "~2.4", // v2.7.3
        "symfony/expression-language": "~2.4", // v2.7.3
        "jms/serializer": "~0.16", // 0.16.0
        "willdurand/hateoas": "~2.3" // v2.3.0
    },
    "require-dev": {
        "behat/mink": "~1.5", // v1.5.0
        "behat/mink-goutte-driver": "~1.0.9", // v1.0.9
        "behat/mink-selenium2-driver": "~1.1.1", // v1.1.1
        "behat/behat": "~2.5", // v2.5.5
        "behat/mink-extension": "~1.2.0", // v1.2.0
        "phpunit/phpunit": "~5.7.0", // 5.7.27
        "guzzle/guzzle": "~3.7" // v3.9.3
    }
}
userVoice