This post is the 2nd part of a previous post available here.
Jane
Now that they can read your swagger-powered documentation, developers start building cool apps using your API: javascript web apps, mobile apps ...
Here comes a developer who asks "Your API and your doc is great but I got to build an HTTP client that builds the right requests for your API, and parse the JSON responses to extract the data I want. Do you know if someone built such an API Client as a SDK so I do not have to rewrite it all ?".
He's right: it is a lot more important for him to focus on his app business logic and UI than to write code that build the expected HTTP requests for your API. He must focus on the "why" he wants to use the API instead of the "how".
What is interesting is that all the details of the "how" are provided in the Swagger file: routes, request body structures, responses formats and http codes, required http headers ...
API Client generation tools
There are a lot of tools that can build an API Client from a Swagger file for multiple programming languages. For PHP, there is Jane OpenAPI. Basically you give it your Swagger file and it generates the needed PHP classes. Pretty neat.
Here is an example of the generated PHP code to perform the GET HTTP call to fetch movies:
/**
* Get movies
*
* @param array $parameters {
* @var string $order Order criterion
* @var string $dir Sort criterion
* @var int $page Page number
* }
* @param string $fetch Fetch mode (object or response)
*
* @return \Psr\Http\Message\ResponseInterface|\ApiCycle\Generated\ApiMoviesClient\Model\MoviesViewDTO
*/
public function getMovies($parameters = array(), $fetch = self::FETCH_OBJECT)
{
$queryParam = new QueryParam();
$queryParam->setDefault('order', NULL);
$queryParam->setDefault('dir', NULL);
$queryParam->setDefault('page', NULL);
$url = '/v1/movies';
$url = $url . ('?' . $queryParam->buildQueryString($parameters));
$headers = array_merge(array('Host' => 'localhost', 'Accept' => array('application/json')), $queryParam->buildHeaders($parameters));
$body = $queryParam->buildFormDataString($parameters);
$request = $this->messageFactory->createRequest('GET', $url, $headers, $body);
$promise = $this->httpClient->sendAsyncRequest($request);
if (self::FETCH_PROMISE === $fetch) {
return $promise;
}
$response = $promise->wait();
if (self::FETCH_OBJECT == $fetch) {
if ('200' == $response->getStatusCode()) {
return $this->serializer->deserialize((string) $response->getBody(), 'ApiCycle\\Generated\\ApiMoviesClient\\Model\\MoviesViewDTO', 'json');
}
}
return $response;
}
So the steps are:
1) generate the API Client classes using Jane
2) autoload them
3) start using them !
I put the generated classes in the generated/
folder of my API Client and I require it in my composer.json to autoload it:
{
"name": "matks/movies-api-client",
"license": "MIT",
"type": "project",
"autoload": {
"psr-4": {
"ApiCycle\\Generated\\ApiMoviesClient\\": "generated/"
}
}
Here an example of how I can use the generated php classes:
// in a Silex app
$resource = $app['app.api.api-client'];
$response1 = $resource->getMovies();
$body = new MoviesBody();
$body->setName('A new movie 2');
$response2 = $resource->createMovie($body);
And next time the API changes, for example /v1/movies
become /v2/api/get-movies
; or the request body for the POST endpoint needs a new mandatory item, I can re-generate my API client to handle the new API requirements. The generated
code is updated, but the code which uses it does not. The "how" is now fully automatic (and more reliable since it is written by a robot instead of a human).
Using behat and the API Client to test the API
Behat is a php framework that allows you to write test scenarios for your app.
Instead of writing tests such as:
public function testGetMoviesWithBadInput()
{
$client = static::createClient();
$crawler = $client->request('GET', '/v1/movies?order=bad');
$response1 = $client->getResponse();
$expectedData1 = [
'message' => 'Only allowed value for order is \'name\'',
'error' => 'Bad query',
];
$this->assertJsonResponse($response1, Response::HTTP_BAD_REQUEST);
$this->assertJsonContent($response1, $expectedData1);
$crawler = $client->request('GET', '/v1/movies?order=name&dir=a');
$response2 = $client->getResponse();
$expectedData2 = [
'message' => 'Dir must be one of those: asc, desc',
'error' => 'Bad query',
];
$this->assertJsonResponse($response2, Response::HTTP_BAD_REQUEST);
$this->assertJsonContent($response2, $expectedData2);
}
You write scenarios that are human-readable such as:
Scenario: Create a movie
When I create a movie "Amazing new movie Z"
Then I should receive a success response
When I fetch page 1 from movies from the Api using an "asc" sorting on names
Then I should receive a list of 3 movies with a total of 31
And the list should contain:
| movie name |
| Amazing new movie Z |
The fact that these scenarios are human-readable makes a big difference: it is so much easier to write, maintain and update. You can even consider it a runnable user-friendly documentation of how your app behaves.
If you want to discover it further, there are multiple tutorials and blog posts available about it such as this one.
Just to sum up how it works:
- you write human-readable scenarios using this "When X Then Y" syntax called Gherkin in .feature files
- you write PHP code which is able to translate these statements into PHP calls in FeatureContexts files
- you run the tests with behat binary
The "translation" between "When I fetch movies from the Api" and an actual PHP call looks like this:
/**
* @When I fetch movies from the Api
*/
public function fetchMoviesBasic()
{
try {
$this->latestResponse = self::getApiClient()->getMovies();
} catch (\Exception $e) {
$this->handleExceptionsAfterAPICall($e);
}
}
This is where I use the generated API Client.
So I wrote a complete features set about my API:
Feature: Movies API
In order to manage movies
As an API Client
I need to be able to use the Movies API
Scenario: Get basic movies list
When I fetch movies from the Api
Then I should receive a list of 3 movies with a total of 30
And the list should contain:
| movie name |
| Fast and Furious 8 |
| Taken 3 |
Scenario: Get advanced movies list
When I fetch page 3 from movies from the Api using an "desc" sorting on names
Then I should receive a list of 3 movies with a total of 30
And the list should contain:
| movie name |
| Another great movie 6 |
Scenario: Create a movie
When I create a movie "Amazing new movie Z"
Then I should receive a success response
When I fetch page 1 from movies from the Api using an "asc" sorting on names
Then I should receive a list of 3 movies with a total of 31
And the list should contain:
| movie name |
| Amazing new movie Z |
Scenario: Create a movie twice and receive a bad response
When I create a movie "Spiderman"
Then I should receive a success response
When I create a movie "Spiderman"
Then I should receive a bad response
Scenario: Delete a movie
When I delete the movie 31
Then I should receive a null response
When I fetch page 1 from movies from the Api using an "asc" sorting on names
Then I should receive a list of 3 movies with a total of 31
And the list should NOT contain:
| movie name |
| Amazing new movie Z |
Scenario: Delete a movie twice and receive a bad response
When I delete the movie 32
Then I should receive a null response
When I delete the movie 32
Then I should receive a bad response
Then I wrote a complete FeatureContext to "translate" all the features into PHP functions which make sense, and it is runnable.
State reset for integration tests
An important point here is that these steps perform real HTTP calls to my API. Which means the API is running with a real database behind (either mysql or sqlite). That is why at the beginning of the test suite I need to reset the API state so that previous test runs do not impact this run.
I performed this with a Makefile-controlled process where I drop the database and recreate it then re-fill it with DataFixtures:
load_fixtures: ## load fixtures
$(APP_CONSOLE) doctrine:schema:create
$(APP_CONSOLE) doctrine:fixtures:load --no-interaction
reset_fixtures: ## load fixtures after cleaning the database
$(APP_CONSOLE) doctrine:schema:drop --force --no-interaction
$(MAKE) load_fixtures
However to be done right, it should be done by my behat test process and not outside of it in a Makefile. Behat allows it with a hooking process where you can perform some steps before the suite or before each scenario or before each step. Guess I'll update the project following this lead soon :) .
Conclusion
I was able to produce a Swagger specification file from my API which can be given to other developers as a documentation or can help them build API clients in whatever language they need. Which is very useful for writing apps that use the API ... or integration tests to check the API behavior is correct.
Tools are awesome :)
Top comments (0)