@adam_englander
BDD API Development with
Symfony and Behat
Adam Englander
LaunchKey Architect, iovation
@adam_englander
Architectural Landscape
@adam_englander
APIs are taking over!
@adam_englander
Mobile Applications
Android
iOS
Windows
Mobile
API
@adam_englander
Single Page Applications
API
@adam_englander
Application Extensibility
API
Your
Shipping
Application Integrations
@adam_englander
Internet of Things
API
@adam_englander
Microservices
Messaging
Distribution
Auth
Orders
Customer
@adam_englander
APIs in Symfony
@adam_englander
Symfony is Extensible
@adam_englander
Your Application Bundle
@adam_englander
Your Application Bundle
Routing
@adam_englander
Your Application Bundle
Routing Serializer
@adam_englander
Your Application Bundle
Routing Firewall Serializer
@adam_englander
Your Application Bundle
Routing Firewall Serializer
Cache
@adam_englander
Your Application Bundle
Routing Firewall Serializer
Event Dispatcher Cache
@adam_englander
Your Application Bundle
Routing Firewall Serializer
Event Dispatcher
Dependency
Cache
@adam_englander
Your Application Bundle
Routing Firewall Serializer
Event Dispatcher
Config
Dependency
Cache
@adam_englander
Your Application Bundle
Routing Firewall Serializer
Event Dispatcher
Config
TranslationDependency
Cache
@adam_englander
Your Application Bundle
Routing Firewall Serializer
Event Dispatcher
Config App Kernels
TranslationDependency
Cache
@adam_englander
Your Application Bundle
Routing Firewall Serializer
Event Dispatcher
Config App Kernels
TranslationDependency
Cache
Full Integration Testing Required
@adam_englander
PHPUnit vs. Behat
@adam_englander
Acceptance
Documentation
Live Tests
Kernel TestsBehat
@adam_englander
Acceptance
Documentation
Live Tests
Kernel TestsPHPUnit
@adam_englander
Behavior Driven Development
is for websites!
@adam_englander
Behavior is Behavior
Web Mobile Device to Device
@adam_englander
Actor 1 Acts -> Actor 2 Responds
Show the
homepage
Here is the
homepage
Adam is home
I turned on
the lights
@adam_englander
Instead of dealing with this:
@adam_englander
Well, actually, this:
<!DOCTYPE html>
<head>
<title>Symfony</title>
</head>
<body>
<div class="position-relative js-header-wrapper">
<a href="#start" tabindex="1" class=“bg-black">
Skip to content
</a>
<div id="loader-bar" class=“pjax-loader-bar”>
<div class=“progress"></div>
</div>
. . .
@adam_englander
You are dealing with this:
HTTP/1.1 200 OK
Date: Thu, 19 Oct 2017 06:28:02 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 1009
Server: GitHub.com
Status: 200 OK
Vary: Accept-Encoding
X-GitHub-Request-Id: CD6C:D7DE:E24F3B2:123F37BB:59E845F1
{
"login": "symfony",
"id": 143937,
"url": "https://api.github.com/orgs/symfony",
. . .
@adam_englander
Feature: All pages require login
As a user
In order to view the home page
I must login to the website
Scenario: Not logged in redirects to login page
When I go to the home page
Then I am redirected to the login page
Scenario: Logged sees page
Given I am logged in
When I go to the home page
Then I see hello Adam
Web
@adam_englander
Feature: All endpoints require OAuth token
As an API consumer
In order to access an endpoint
I must be authenticated
Scenario: Not logged in shows 401
When I access the status endpoint
Then the HTTPS Status is 401 Unauthorized
And the WWW-Authenticate header is Bearer
realm=“API Realm”
Scenario: Authorized returns endpoint response
Given a valid OAuth Token
When I access the status endpoint
Then the HTTP Status is 200 OK
Device to
Device
@adam_englander
That’s nice and all. How do I do
Kernel and Live tests?
@adam_englander
Multi-Profile — Multi-Client
@adam_englander
Default Profile: Symfony Extension
default:
extensions:
BehatSymfony2Extension: ~
suites:
default:
contexts: [SymfonyKernelFeatureContext]
@adam_englander
Server Profile: Guzzle Client
server:
extensions: []
suites:
default:
contexts:
- GuzzleFeatureContext:
- base_uri: http://localhost:8000
@adam_englander
Context Hierarchy
Abstract
Context
Symfony
Kernel
Context
Guzzle
Client
Context
@adam_englander
Abstract Context
protected abstract function sendRequestImpl(
RequestInterface $request): ResponseInterface;
/**
* @Then /^the response status code is (d+)$/
*/
public function theResponseStatusCodeIs($statusCode)
{
MatcherAssert::assertThat(
$this->response->getStatusCode(),
Matchers::equalTo($statusCode)
);
}
@adam_englander
Guzzle Client Context
"require-dev": {
"guzzlehttp/guzzle": “^6.3”,
. . .
@adam_englander
Guzzle Client Context
public function __construct($config)
{
$this->client = new GuzzleHttpClient($config);
}
@adam_englander
Guzzle Client Context
protected function sendRequestImpl($request) {
try {
$response = $this->client->send($request);
} catch (ClientException $clientException) {
$response = $clientException->getResponse();
}
return $response;
}
@adam_englander
Symfony Kernel Context
"require-dev": {
"behat/symfony2-extension": “^2.1.1",
"symfony/psr-http-message-bridge": "^1.0",
"zendframework/zend-diactoros": "^1.4"
. . .
}
@adam_englander
Symfony Kernel Context
class SymfonyKernelFeatureContext
extends AbstractFeatureContext
implements KernelAwareContext
public function __construct() {
$this->psr7Factory = new DiactorosFactory();
}
public function setKernel(KernelInterface $kernel) {
$this->client = $kernel
->getContainer()->get(‘test.client');
}
@adam_englander
Symfony Kernel Context
protected function sendRequestImpl($request) {
$this->client->request(
$request->getMethod(),
$request->getUri()->getPath());
$clientResponse = $this->client->getResponse();
$response = $this->psr7Factory
->createResponse($clientResponse);
return $response;
}
@adam_englander
That Was Easy!
@adam_englander
Lessons From the Trenches
@adam_englander
LaunchKey MFA API
41 Unique paths
130 handler methods
~10K lines of code
120 libraries
@adam_englander
LaunchKey MFA API Tests
55 Features
328 Scenarios
~3K lines of step code
@adam_englander
Split feature files by functional
features not by path.
@adam_englander
Break functionality into multiple
contexts like you would bundles.
@adam_englander
If you have complex step code,
unit test that code!
@adam_englander
Expose endpoints for setup and
teardown.
@adam_englander
Test a feature only once. Every step
that is not testing the feature is a
Given.
@adam_englander
Every Given needs to be tested
in another Feature.
@adam_englander
https://joind.in/talk/ead28

Symfony Live San Franciso 2017 - BDD API Development with Symfony and Behat