Łukasz Chruściel30/12/2015

TDD your API with Symfony and PHPUnit

At Lakion we truly believe in TDD approach. We are convinced that rapid development can not be achieved without proper testing and SOLID code. We always start with defining our needs and writing scenarios, specifications or tests that reflect them. Thanks to this approach, we get exactly what we need and tests are protecting us from regressions.

Our Open Source project - Sylius is developed using the Full-Stack BDD methodology. You should always choose a tool that is the most suitable for the job. Behat and phpspec don't have many secrets for us because we are using them on a daily basis. Nevertheless, with one of big projects we have decided to add another tool to our TDD toolbelt. We have come up with a very simple but useful PHPUnit test case for testing APIs. - ApiTestCase.

Why ApiTestCase?

We had to develop several Symfony applications communicating with each other via JSON API. Multiple developers were working on them simultaneously. Yet they had to implemented as if everything existed and worked already. So how do you do that? We had to define contracts - requests and expected responses. We decided to put the latter in simple .json files and use them for our TDD workflow. Then we just had to prepare a request in our test suite, send the request and compare the result with our expectations. What is more we could agree on a specific JSON response of the other app, define it in a separate file and easily mock the responses from our own and third-party services. That allowed us to develop the applications in parallel and without blocking each other.

To improve the developer experience when working with the ApiTestCase, we have extended the basic Symfony WebTestCase with PHP-Matcher, SymfonyMockerContainer and Alice libraries. The first one allows to assert unpredictable values, such as object ids, current dates and etc. The second one simplifies the mocking process. The third one allows you to load sample data with ease. Big "Thank you" to the community for these excellent tools!

But how does it work in practice? Let me show you #TheLakionWay.

Rapid TDD API Development with Lionframe and ApiTestCase

Let's assume that we are creating a very simple e-commerce catalog API. How quickly we can create a full CRUD API following the Test-Driven-Development workflow? Have a look!

Set Up The Project

You can do all this with plain Symfony but we have prepared a slightly modified Symfony-Standard Edition, which has all the API goodies pre-installed. We call it Lionframe. It allows you to develop powerful REST APIs with ease, while supporting the best practices.

composer create-project lakion/lionframe project/ 0.3.0 --prefer-dist
cd project/

You can see example use of Lionframe and ApiTestCase in the DemoBundle directory but let's create our own API. We should start from removing the DemoBundle:

  • Delete the ``src/DemoBundle`` directory;
  • Edit ``app/config/api.yml`` and remove the first six lines:
sylius_resource:
    resources:
        acme.post:
            driver: doctrine/orm
            classes:
                model: DemoBundle\Entity\Post
  • Remove `new DemoBundle\DemoBundle()` reference in ``app/AppKernel.php``;
  • Remove routing importing in ``app/config/routing.yml``;
acme_demo:
    resource: "@DemoBundle/Resources/config/routing.yml"
    prefix:   /

Finally, let's create an empty database:

app/console doctrine:database:create

That's it, we can get our hands dirty with some code!

Write Your Test!

With our needs defined we can write our first test. Let's start with a creation step. To create a product we need to provide the following data:

{
    "name": "Star Wars T-Shirt",
    "sku": "SWTS",
    "price": 50
}

And send it to /products/ endpoint. The test should be placed under src/AppBundle/Tests/Controller/ directory in the ProductApiTest.php file and looks like this:

This test will ensure that the data we sent will result in a proper object creation. To check the response we should define our expectation in src/AppBundle/Tests/Responses/Expected/products/create_response.json file.

{
    "id": @integer@,
    "name": "Star Wars T-Shirt",
    "sku": "SWTS",
    "price": 50
}

Let's run our test now. According to the TDD worfklow, we should see some red color:

bin/phpunit

Defining Expected Responses

We will use a Product entity with the following fields:

  • Id (integer)
  • Name (string)
  • Sku (string)
  • Price (integer)

Let's Make It Green

It's high time to write some code, isn't it? If you know Symfony basics, you will know that to turn this test green we will need following things:

  • Product entity
  • Controller
  • Routing

Of course, it is super simple to implement that in raw Symfony but who has the time to do the same thing all over again? We need to be rapid! That's where Lionframe comes into play.

Symfony has a simple generate command, which will create the entity for us. We suggest using the yml format. The class will use AppBundle:Product as shortcut name. Remember that id field will be generated by default. Run:

app/console generate:doctrine:entity

You should see something like this output below, just follow instructions:

Welcome to the Doctrine2 entity generator  

This command helps you generate Doctrine2 entities.

First, you need to give the entity name you want to generate.
You must use the shortcut notation like AcmeBlogBundle:Post.

The Entity shortcut name: AppBundle:Product

Determine the format to use for the mapping information.

Configuration format (yml, xml, php, or annotation) [annotation]: yml

Instead of starting with a blank entity, you can add some fields now.
Note that the primary key will be added automatically (named id).

Available types: array, simple_array, json_array, object, 
boolean, integer, smallint, bigint, string, text, datetime, datetimetz, 
date, time, decimal, float, binary, blob, guid.

New field name (press  to stop adding fields): name
Field type [string]: 
Field length [255]: 

New field name (press  to stop adding fields): sku
Field type [string]: 
Field length [255]: 

New field name (press  to stop adding fields): price
Field type [string]: integer

New field name (press  to stop adding fields): 

Do you want to generate an empty repository class [no]? 

  Summary before generation  

You are going to generate a "AppBundle:Product" Doctrine2 entity
using the "yml" format.

Do you confirm generation [yes]? 

  Entity generation  

Generating the entity code: OK

You can now start using the generated code!  

Finally, you can create your database schema:

app/console doctrine:schema:create

At this point we should create a controller and define proper routing. You do not have to bother with tedious writing of repetitive code for standard CRUD actions.

Let's finally put Lionframe to work. Register your new entity as a Sylius resource in app/config/api.yml

sylius_resource:
    resources:
        app.product:
            classes:
                model: AppBundle\Entity\Product

And add this routing configuration to app/config/routing.yml:

app_product_api:
    resource: app.product
    type: sylius.api

Now run your tests again.

bin/phpunit

What color do you see? You can try it yourself. Spin up the built-in server and do some requests:

php app/console server:run

Create a product via CURL:

curl -i -X POST -H "Content-Type: application/json" -d '{"name": "Awesome Mug", "sku": "MUG-492", "price": 499}' http://127.0.0.1:8000/products/

Refactor

In this case, Lionframe with SyliusResourceBundle inside takes care of all the heavy-lifting but if you prefer to implement controllers on your own, then you could go now and refactor your code and ApiTestCase will ensure everything still works. This workflow allows to maintain very high quality of code in the project and makes your life as a developer much easier.

While this examples shows REST-style CRUD, keep in mind that you can test pretty much any type of API.

Not a fan of JSON?

We got you covered. ApiTestCase supports XML too. Because necessity is the mother of invention, our colleague @Arminek implemented XML support. Testing SOAP APIs or any other that uses XML messages have never been so easy!

Summary

There is no rapid development without testing. Quoting Uncle Bob: "The only way to go fast is to go well". You can develop something really quickly but without testing the technical debt will slow you down very soon and every change will be very costly. ApiTestCase allows you to change your API very easily: Change the test or expected response, see red color and then worry only about making it green again. That's why we have created ApiTestCase and Lionframe. These two will be your best buddies in the world of API development and save you a lot of time.

Useful Documentation Links:


Want to see how we work?

This website uses cookies to improve your experience. We’ll assume you are ok with this, but you can opt-out if you wish, and read more about it or just close it.