@stadolf @coding_earth
The Smart Shelf
api-platform in an IoT context
TESTING
2
Stefan Adolf
Developer Ambassador
#javascript #mongodb #serverless
#blockchain #codingberlin #elastic
#aws #php #symfony #react
#digitalization #agile #b2b #marketplaces
#spryker #php #k8s #largescale
#turbinejetzt #iot #ux #vuejs
@stadolf
elmariachi111
May 25th
Festsaal Kreuzberg
12 code heavy talks
https://devday.io #devday19
2 locations / live streamed lunch included 2 panel discussions
live coding challenge powered by platform.sh
use DEVDAYLOVESPHP and go there for €10!
@stadolf @coding_earth devday.io DEVDAYLOVESPHP
Behaviour Driven Development
๏allow developers, “QA”- and business participants to collaborate
๏formalization of application behaviour
๏use a simple natural language DSL to describe behaviour
@stadolf @coding_earth devday.io DEVDAYLOVESPHP
PHP: Behat
๏a “StoryBDD” tool for PHP
๏phpspec: SpecBDD
๏around since ~2011
๏uses Gherkin language to describe features and scenarios
๏every feature consists of scenarios, each scenario of steps, each step of Givens, Whens, Thens
๏Every scenario is isolated from each other scenario’s context
๏every scenario gets its own context instance
๏every step can access (changed) variables of its context
@stadolf @coding_earth devday.io DEVDAYLOVESPHP
GherkinFeature: Make my country great again
In order to improve the world
As a the leader of the greatest country in the world
I must take actions to make my country even greater
Background: #https://www.cdc.gov/nchs/fastats/deaths.htm
Given There is a country called "USA" with 327167434 citizens
And "Donald Trump" is its president
And birth rate is 13 per 1000
And "55" out of "10000" citizens die because of "natural reasons"
And "4" out of "10000" citizens die because of "accidents"
And "13" out of "100000" citizens die because of "suicide"
And "5" out of "100000" citizens die because of "homicide"
And "53691" new citizens arrive as "refugees"
Scenario:
When The president runs that country for "8" years
Then There should me more citizens in the country than 340000000
And There should me less citizens in the country than 350000000
@stadolf @coding_earth devday.io DEVDAYLOVESPHP
Behat.ymldefault:
autoload: [ 'features/contexts' ]
suites:
default:
paths: [ 'features' ]
contexts:
- FeatureContext:
kernel: "@kernel"
- DomainContext:
logger: "@test.app.logger"
extensions:
BehatSymfony2Extension:
kernel:
bootstrap: features/bootstrap/bootstrap.php
class: AppKernel
+ symfony 2 extension
@stadolf @coding_earth devday.io DEVDAYLOVESPHP
code generation
Scenario: restrict weapons possession # features/greatcountries.feature:27
When I restrict the right to possess firearms
And The president runs that country for 10 years # DomainContext::thePresidentRunsThatCountryForYears()
Then The "homicide" death rate should be "1" out of "100000"
>> default suite has undefined steps. Please choose the context to generate snippets:
[2] DomainContext
--- DomainContext has missing steps. Define them with these snippets:
/**
* @When I restrict the right to possess firearms
*/
public function iRestrictTheRightToPossessFirearms()
{
throw new PendingException();
}
/**
* @Then The :arg1 death rate should be :arg2 out of :arg3
*/
public function theDeathRateShouldBeOutOf($arg1, $arg2, $arg3)
{
throw new PendingException();
}
@stadolf @coding_earth devday.io DEVDAYLOVESPHP
DEMO: BEHAT
@stadolf @coding_earth devday.io DEVDAYLOVESPHP
Behat ❤ api-platform
MAKE THESE SMART
@stadolf @coding_earth
1. A SHELF
@stadolf @coding_earth
2. SENSORS
@stadolf @coding_earth
3. A CONCEPT
@stadolf @coding_earth
SMART!
@stadolf @coding_earth devday.io DEVDAYLOVESPHP
"location_id": "shelf1",
"payload": {
"battery_mV": 4110,
"build": "20181118-214917",
"distance_mm": 518,
"type": "distance-sensor-firmware",
"version": "1.0"
},
"sensor_id": "A41E9640"
}
POST /sensor/receive_event
@stadolf @coding_earth devday.io DEVDAYLOVESPHP
= API out of the box.
๏docker / docker-compose dev env
๏API with 1 demo entity
๏json-ld / hal serializer (data + links +
metadata + schema)
๏json-api support if you need it
๏hydra docs provided in <Link>
headers
๏swagger.json (OpenAPI v2 / v3)
๏autogenerated interactive docs
(NelmioApiDocs) & ReDocs
๏auto generated administration
interface (React-Admin)
๏scaffolder for React & Vue PWAs
๏Mercure for realtime http/2 pushes
๏h2 proxy / Varnish support
๏Helm charts for K8S deployment
@stadolf @coding_earth
Organization
API Resources
UserUser
Shelf Shelf
Slot Slot Slot Slot
ProductStream
Product
Supplier
A123 A654 A787
dim dim
A787
dim dim
ProductStream
Supplier
Product
dim dim
Slot
A123

999
@stadolf @coding_earth devday.io DEVDAYLOVESPHP
/**
* @ORMEntity
* @ORMHasLifecycleCallbacks()
* @ApiResource(
* attributes={
* "access_control"="is_granted('ROLE_USER')",
* "order"={"product.title": "ASC"}
* }
* )
* @ApiFilter(SearchFilter::class, properties={"product.title": "partial"})
*/
class ProductStream implements OrganizationAwareResourceInterface
{
/**
* @var int
* @ORMId
* @ORMGeneratedValue
* @ORMColumn(type="integer")
* @Groups({"product_stream", "products", "sensor", "receive_event"})
*/
private $id;
/**
* @var string
* @ORMColumn(type="string", length=255)
* @Groups({"product_stream", "products", "shelf"})
*/
protected $title;
/**
* @var Product
* @ORMManyToOne(targetEntity="Product", inversedBy="productStreams")
* @Groups({"product_stream", "shelf", "slot", "sensor"})
*/
protected $product;
Declaration
Filtering
Serialization groups
Relationships
Access control
this is the only “code” I write.
@stadolf @coding_earth devday.io DEVDAYLOVESPHP
just by adding declarations
to get this.
@stadolf @coding_earth devday.io DEVDAYLOVESPHP
GraphQL included.
@stadolf @coding_earth devday.io DEVDAYLOVESPHP
class AssignProductToSlot
{
public function __invoke(Slot $data, $product_id)
{
$product = $this->em->getRepository(Product::class)->find($product_id);
if (!$product) {
throw new NotFoundHttpException("product $product_id unknown");
}
$organization = $data->getShelf()->getOrganization();//$data->getShelf()->getOrganization()
$productStreamRepo = $this->em->getRepository(ProductStream::class);
$productStream = $productStreamRepo->findOneBy(['organization' => $organization, 'product' => $product_id]);
if ($productStream) {
$this->logger->info("reusing product stream for slot");
$data->setProductStream($productStream);
} else {
$productStream = new ProductStream();
$productStream->setOrganization($organization)
->setProduct($product)
->setTitle($product->getTitle())
->addSlot($data)
;
$this->logger->info("created new product stream for product [{$product->getTitle()}]");
$this->em->persist($productStream);
}
//update the slot with latest sensor data
if ($data->getSensor()) {
$latestSensorEvent = $this->eventStorage->getLatestSensorEvent($data->getSensor()->getDeviceId());
custom operations
PUT /slot/:slot/product/:product
1. get or create product stream
2. assign product
3. if a sensor is already assigned:
4. update the stream with current status
@stadolf @coding_earth devday.io DEVDAYLOVESPHP
@stadolf @coding_earth devday.io DEVDAYLOVESPHP
BDD TEST YOUR API
@stadolf @coding_earth devday.io DEVDAYLOVESPHP
Challenges
๏test “real” requests against the API
๏authentication, headers, response codes, request bodies
๏deal with database fixtures
๏integrate external contexts
๏mock the JWT authentication
๏test several API formats (GraphQL)
@stadolf @coding_earth devday.io DEVDAYLOVESPHP
Feature: Deliver sensor events
Background:
Given There is an Organization titled "organization"
And The organization has a Shelf titled "shelf1"
And The user "user@organization.com" exists and has the role "ROLE_USER"
And The shelf has a slot called "links oben" with dimensions '{"width":300, "height": 400, "length": 1250}'
And the product "Ice Tea" exists with packageDimensions '{"width":300, "height": 300, "length": 400}'



@createSchema
Scenario: Push another event, create a slot and associate it with a product stream
When I add "Content-Type" header equal to "application/ld+json"
And I add "Accept" header equal to "application/ld+json"
And I authenticate using the email address "gateway1@resourceful.de" having the role "ROLE_GATEWAY"
And I send a "POST" request to "/sensor/receive_event" with body:
"""
{
"sensor_id": “A123457",
"payload": {
"distance_mm": 950,
"battery_mV": 3500
},
"location_id": "shelve"
}
"""
Then the response status code should be 201
When I send a "PUT" request to "/slots/2/products/1"
Then the response status code should be 200
Testing the REST APIhttp://behat.org/en/latest/
sensorevents.feature
@stadolf @coding_earth devday.io DEVDAYLOVESPHP
/**
* @Then the ProductStream for product :productTitle contains :count items
*/
public function theProductstreamForContainsItems($productTitle, $count)
{
$product = $this->em->getRepository(Product::class)->findOneBy(["title" => $productTitle]);
$productStream = $product->getProductStreams()->first();
foreach ($productStream->getSlots() as $slot) {
$slot = $this->findOrRefresh($slot);
}
$this->assertEquals($count, $productStream->getCurrentBoxCount());
}
Testing domain logic
ResourcefulContext.php
@stadolf @coding_earth devday.io DEVDAYLOVESPHP
class FeatureContext implements Context
{
public function __construct(ManagerRegistry $doctrine, Request $request)
{
$this->doctrine = $doctrine;
$this->manager = $doctrine->getManager();
$this->schemaTool = new SchemaTool($this->manager);
$this->classes = $this->manager->getMetadataFactory()->getAllMetadata();
}
/**
* @BeforeScenario @createSchema
*/
public function createDatabase()
{
$this->schemaTool->dropSchema($this->classes);
$this->doctrine->getManager()->clear();
$this->schemaTool->createSchema($this->classes);
}
}
rebuild fixturesFeatureContext
drop the database!
@stadolf @coding_earth devday.io DEVDAYLOVESPHP
Background:
Given There is an Organization titled "organization"
And The organization has a Shelf titled "shelf1"
And The user "user@organization.com" exists and has the role "ROLE_USER"
And The shelf has a slot called "links oben" with dimensions 

'{"width":300, "height": 400, "length": 1250}'
And the product "Ice Tea" exists with packageDimensions 

'{"width":300, "height": 300, "length": 400}'



rebuild fixturessensorevents.feature
@stadolf @coding_earth devday.io DEVDAYLOVESPHP
JWT Authentication
security:
providers:
jwt:
lexik_jwt:
class: AppEntityUser
app_user_provider:
entity:
class: AppEntityUser
property: email
encoders:
AppEntityUser:
algorithm: argon2i
firewalls:
login:
pattern: ^/login
stateless: true
anonymous: true
provider: app_user_provider
json_login:
check_path: /login_check
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
main:
pattern: ^/
provider: jwt
stateless: true
guard:
authenticators:
- lexik_jwt_authentication.jwt_token_authenticator
provider: jwt
anonymous: ~
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: IS_AUTHENTICATED_ANONYMOUSLY }
database-less authentication
JWT creation endpoint
https://packagist.org/packages/lexik/jwt-authentication-bundle
@stadolf @coding_earth devday.io DEVDAYLOVESPHP
Mock authentication
/**
* @When I authenticate using the email address :arg1 having the role :arg2
*/
public function iAuthenticateAs($email, $role)
{
$user = $this->em->getRepository(User::class)->findOneBy(['email' => $email]);
if (!$user) {
$user = $this->userManager->createUser($email, $email);
$user->setRoles([$role]);
$this->em->persist($user);
$this->em->flush();
}
$jwtToken = $this->jwtManager->create($user);
$this->request->setHttpHeader("Authorization", "Bearer $jwtToken");
}
create JWT on the fly
BehatchRestContext
AuthenticationContext
class AuthenticationContext extends RestContext
@stadolf @coding_earth devday.io DEVDAYLOVESPHP
default:
autoload: [ 'features/contexts' ]
suites:
default:
paths: [ 'features' ]
contexts:
- AuthenticationContext:
doctrine: "@doctrine"
jwtManager: "@lexik_jwt_authentication.jwt_manager"
userManager: "@AppSecurityUserManager"
- FeatureContext:
doctrine: "@doctrine"
- ResourcefulContext:
doctrine: "@doctrine"
userManager: "@AppSecurityUserManager"
- BehatMinkExtensionContextMinkContext

- GraphqlContext
- behatch:context:json:
evaluationMode: javascript
- behatch:context:system:
root: “."
- behatch:context:table
- behatch:context:xml
extensions:
BehatSymfony2Extension:
kernel:
bootstrap: config/behat_bootstrap.php
class: AppKernel
BehatchExtension: ~
BehatMinkExtension:
sessions:
default:
Integrate contexts
“When I authenticate as “
“I send a "POST" request to“
“there is an organization…”
use the Symfony Kernel & DI
behat.yml
@stadolf @coding_earth devday.io DEVDAYLOVESPHP
Integrate Contexts
<?php
final class GraphqlContext implements Context
{
/** @var BehatchContextRestContext */
private $restContext;
private $request;
public function __construct(Request $request) {
$this->request = $request;
}
/**
* Gives access to the Behatch context.
* @BeforeScenario
*/
public function gatherContexts(BeforeScenarioScope $scope) {
/** @var InitializedContextEnvironment $environment */
$environment = $scope->getEnvironment();
$this->restContext = $environment->getContext(AuthenticationContext::class);
}
/** @When I send the following GraphQL request: */
public function ISendTheFollowingGraphqlRequest(PyStringNode $request) {
$this->IHaveTheFollowingGraphqlRequest($request);
$this->request->setHttpHeader('Accept', null);
$this->restContext->iSendARequestTo('GET', '/graphql?' . http_build_query($this->graphqlRequest));
}
get REST context
use REST context
@stadolf @coding_earth devday.io DEVDAYLOVESPHP
@createSchema
Scenario: query all products
When I add "Content-Type" header equal to "application/ld+json"
And I add "Accept" header equal to "application/ld+json"
And I authenticate using the email address "user@organization.com" having the role "ROLE_USER"
And I send the following GraphQL request:
"""
{
products {
edges {
node {
title
width
}
}
}
}
"""
Then the response status code should be 200
And the response should be in JSON
And the JSON node "data.products.edges[0].node.title" should be equal to "Ice Tea"
And the JSON node "data.products.edges[0].node.width" should be equal to 300
Testing GraphQLhttp://behat.org/en/latest/
sensorevents.feature
affects the REST Request
@stadolf @coding_earth devday.io DEVDAYLOVESPHP
DEMO: BEHAT 

FOR API-PLATFORM
@stadolf @coding_earth devday.io DEVDAYLOVESPHP
Too long, didn’t listen
๏BDD is a great tool to define features in real language
๏Behat can be used for API testing
๏this way it’s closer to SpecBDD than StoryBDD
๏Don’t test everything like this - CI performance will kill you
๏it integrates nicely with api-platform
๏api-platform is great to build APIs.
@stadolf @coding_earth devday.io DEVDAYLOVESPHP
That’s all, folks
Stefan Adolf

https://twitter.com/stadolf

https://github.com/elmariachi111

https://www.linkedin.com/in/stadolf/
@stadolf @coding_earth devday.io DEVDAYLOVESPHP
@stadolf @coding_earth devday.io DEVDAYLOVESPHP
Q&A

Testing API platform with Behat BDD tests

  • 1.
    @stadolf @coding_earth The SmartShelf api-platform in an IoT context TESTING
  • 2.
    2 Stefan Adolf Developer Ambassador #javascript#mongodb #serverless #blockchain #codingberlin #elastic #aws #php #symfony #react #digitalization #agile #b2b #marketplaces #spryker #php #k8s #largescale #turbinejetzt #iot #ux #vuejs @stadolf elmariachi111
  • 3.
    May 25th Festsaal Kreuzberg 12code heavy talks https://devday.io #devday19 2 locations / live streamed lunch included 2 panel discussions live coding challenge powered by platform.sh use DEVDAYLOVESPHP and go there for €10!
  • 4.
    @stadolf @coding_earth devday.ioDEVDAYLOVESPHP Behaviour Driven Development ๏allow developers, “QA”- and business participants to collaborate ๏formalization of application behaviour ๏use a simple natural language DSL to describe behaviour
  • 5.
    @stadolf @coding_earth devday.ioDEVDAYLOVESPHP PHP: Behat ๏a “StoryBDD” tool for PHP ๏phpspec: SpecBDD ๏around since ~2011 ๏uses Gherkin language to describe features and scenarios ๏every feature consists of scenarios, each scenario of steps, each step of Givens, Whens, Thens ๏Every scenario is isolated from each other scenario’s context ๏every scenario gets its own context instance ๏every step can access (changed) variables of its context
  • 6.
    @stadolf @coding_earth devday.ioDEVDAYLOVESPHP GherkinFeature: Make my country great again In order to improve the world As a the leader of the greatest country in the world I must take actions to make my country even greater Background: #https://www.cdc.gov/nchs/fastats/deaths.htm Given There is a country called "USA" with 327167434 citizens And "Donald Trump" is its president And birth rate is 13 per 1000 And "55" out of "10000" citizens die because of "natural reasons" And "4" out of "10000" citizens die because of "accidents" And "13" out of "100000" citizens die because of "suicide" And "5" out of "100000" citizens die because of "homicide" And "53691" new citizens arrive as "refugees" Scenario: When The president runs that country for "8" years Then There should me more citizens in the country than 340000000 And There should me less citizens in the country than 350000000
  • 7.
    @stadolf @coding_earth devday.ioDEVDAYLOVESPHP Behat.ymldefault: autoload: [ 'features/contexts' ] suites: default: paths: [ 'features' ] contexts: - FeatureContext: kernel: "@kernel" - DomainContext: logger: "@test.app.logger" extensions: BehatSymfony2Extension: kernel: bootstrap: features/bootstrap/bootstrap.php class: AppKernel + symfony 2 extension
  • 8.
    @stadolf @coding_earth devday.ioDEVDAYLOVESPHP code generation Scenario: restrict weapons possession # features/greatcountries.feature:27 When I restrict the right to possess firearms And The president runs that country for 10 years # DomainContext::thePresidentRunsThatCountryForYears() Then The "homicide" death rate should be "1" out of "100000" >> default suite has undefined steps. Please choose the context to generate snippets: [2] DomainContext --- DomainContext has missing steps. Define them with these snippets: /** * @When I restrict the right to possess firearms */ public function iRestrictTheRightToPossessFirearms() { throw new PendingException(); } /** * @Then The :arg1 death rate should be :arg2 out of :arg3 */ public function theDeathRateShouldBeOutOf($arg1, $arg2, $arg3) { throw new PendingException(); }
  • 9.
    @stadolf @coding_earth devday.ioDEVDAYLOVESPHP DEMO: BEHAT
  • 10.
    @stadolf @coding_earth devday.ioDEVDAYLOVESPHP Behat ❤ api-platform
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
    @stadolf @coding_earth devday.ioDEVDAYLOVESPHP "location_id": "shelf1", "payload": { "battery_mV": 4110, "build": "20181118-214917", "distance_mm": 518, "type": "distance-sensor-firmware", "version": "1.0" }, "sensor_id": "A41E9640" } POST /sensor/receive_event
  • 17.
    @stadolf @coding_earth devday.ioDEVDAYLOVESPHP = API out of the box. ๏docker / docker-compose dev env ๏API with 1 demo entity ๏json-ld / hal serializer (data + links + metadata + schema) ๏json-api support if you need it ๏hydra docs provided in <Link> headers ๏swagger.json (OpenAPI v2 / v3) ๏autogenerated interactive docs (NelmioApiDocs) & ReDocs ๏auto generated administration interface (React-Admin) ๏scaffolder for React & Vue PWAs ๏Mercure for realtime http/2 pushes ๏h2 proxy / Varnish support ๏Helm charts for K8S deployment
  • 18.
    @stadolf @coding_earth Organization API Resources UserUser ShelfShelf Slot Slot Slot Slot ProductStream Product Supplier A123 A654 A787 dim dim A787 dim dim ProductStream Supplier Product dim dim Slot A123
 999
  • 19.
    @stadolf @coding_earth devday.ioDEVDAYLOVESPHP /** * @ORMEntity * @ORMHasLifecycleCallbacks() * @ApiResource( * attributes={ * "access_control"="is_granted('ROLE_USER')", * "order"={"product.title": "ASC"} * } * ) * @ApiFilter(SearchFilter::class, properties={"product.title": "partial"}) */ class ProductStream implements OrganizationAwareResourceInterface { /** * @var int * @ORMId * @ORMGeneratedValue * @ORMColumn(type="integer") * @Groups({"product_stream", "products", "sensor", "receive_event"}) */ private $id; /** * @var string * @ORMColumn(type="string", length=255) * @Groups({"product_stream", "products", "shelf"}) */ protected $title; /** * @var Product * @ORMManyToOne(targetEntity="Product", inversedBy="productStreams") * @Groups({"product_stream", "shelf", "slot", "sensor"}) */ protected $product; Declaration Filtering Serialization groups Relationships Access control this is the only “code” I write.
  • 20.
    @stadolf @coding_earth devday.ioDEVDAYLOVESPHP just by adding declarations to get this.
  • 21.
    @stadolf @coding_earth devday.ioDEVDAYLOVESPHP GraphQL included.
  • 22.
    @stadolf @coding_earth devday.ioDEVDAYLOVESPHP class AssignProductToSlot { public function __invoke(Slot $data, $product_id) { $product = $this->em->getRepository(Product::class)->find($product_id); if (!$product) { throw new NotFoundHttpException("product $product_id unknown"); } $organization = $data->getShelf()->getOrganization();//$data->getShelf()->getOrganization() $productStreamRepo = $this->em->getRepository(ProductStream::class); $productStream = $productStreamRepo->findOneBy(['organization' => $organization, 'product' => $product_id]); if ($productStream) { $this->logger->info("reusing product stream for slot"); $data->setProductStream($productStream); } else { $productStream = new ProductStream(); $productStream->setOrganization($organization) ->setProduct($product) ->setTitle($product->getTitle()) ->addSlot($data) ; $this->logger->info("created new product stream for product [{$product->getTitle()}]"); $this->em->persist($productStream); } //update the slot with latest sensor data if ($data->getSensor()) { $latestSensorEvent = $this->eventStorage->getLatestSensorEvent($data->getSensor()->getDeviceId()); custom operations PUT /slot/:slot/product/:product 1. get or create product stream 2. assign product 3. if a sensor is already assigned: 4. update the stream with current status
  • 23.
  • 24.
    @stadolf @coding_earth devday.ioDEVDAYLOVESPHP BDD TEST YOUR API
  • 25.
    @stadolf @coding_earth devday.ioDEVDAYLOVESPHP Challenges ๏test “real” requests against the API ๏authentication, headers, response codes, request bodies ๏deal with database fixtures ๏integrate external contexts ๏mock the JWT authentication ๏test several API formats (GraphQL)
  • 26.
    @stadolf @coding_earth devday.ioDEVDAYLOVESPHP Feature: Deliver sensor events Background: Given There is an Organization titled "organization" And The organization has a Shelf titled "shelf1" And The user "user@organization.com" exists and has the role "ROLE_USER" And The shelf has a slot called "links oben" with dimensions '{"width":300, "height": 400, "length": 1250}' And the product "Ice Tea" exists with packageDimensions '{"width":300, "height": 300, "length": 400}'
 
 @createSchema Scenario: Push another event, create a slot and associate it with a product stream When I add "Content-Type" header equal to "application/ld+json" And I add "Accept" header equal to "application/ld+json" And I authenticate using the email address "gateway1@resourceful.de" having the role "ROLE_GATEWAY" And I send a "POST" request to "/sensor/receive_event" with body: """ { "sensor_id": “A123457", "payload": { "distance_mm": 950, "battery_mV": 3500 }, "location_id": "shelve" } """ Then the response status code should be 201 When I send a "PUT" request to "/slots/2/products/1" Then the response status code should be 200 Testing the REST APIhttp://behat.org/en/latest/ sensorevents.feature
  • 27.
    @stadolf @coding_earth devday.ioDEVDAYLOVESPHP /** * @Then the ProductStream for product :productTitle contains :count items */ public function theProductstreamForContainsItems($productTitle, $count) { $product = $this->em->getRepository(Product::class)->findOneBy(["title" => $productTitle]); $productStream = $product->getProductStreams()->first(); foreach ($productStream->getSlots() as $slot) { $slot = $this->findOrRefresh($slot); } $this->assertEquals($count, $productStream->getCurrentBoxCount()); } Testing domain logic ResourcefulContext.php
  • 28.
    @stadolf @coding_earth devday.ioDEVDAYLOVESPHP class FeatureContext implements Context { public function __construct(ManagerRegistry $doctrine, Request $request) { $this->doctrine = $doctrine; $this->manager = $doctrine->getManager(); $this->schemaTool = new SchemaTool($this->manager); $this->classes = $this->manager->getMetadataFactory()->getAllMetadata(); } /** * @BeforeScenario @createSchema */ public function createDatabase() { $this->schemaTool->dropSchema($this->classes); $this->doctrine->getManager()->clear(); $this->schemaTool->createSchema($this->classes); } } rebuild fixturesFeatureContext drop the database!
  • 29.
    @stadolf @coding_earth devday.ioDEVDAYLOVESPHP Background: Given There is an Organization titled "organization" And The organization has a Shelf titled "shelf1" And The user "user@organization.com" exists and has the role "ROLE_USER" And The shelf has a slot called "links oben" with dimensions 
 '{"width":300, "height": 400, "length": 1250}' And the product "Ice Tea" exists with packageDimensions 
 '{"width":300, "height": 300, "length": 400}'
 
 rebuild fixturessensorevents.feature
  • 30.
    @stadolf @coding_earth devday.ioDEVDAYLOVESPHP JWT Authentication security: providers: jwt: lexik_jwt: class: AppEntityUser app_user_provider: entity: class: AppEntityUser property: email encoders: AppEntityUser: algorithm: argon2i firewalls: login: pattern: ^/login stateless: true anonymous: true provider: app_user_provider json_login: check_path: /login_check success_handler: lexik_jwt_authentication.handler.authentication_success failure_handler: lexik_jwt_authentication.handler.authentication_failure main: pattern: ^/ provider: jwt stateless: true guard: authenticators: - lexik_jwt_authentication.jwt_token_authenticator provider: jwt anonymous: ~ access_control: - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/, roles: IS_AUTHENTICATED_ANONYMOUSLY } database-less authentication JWT creation endpoint https://packagist.org/packages/lexik/jwt-authentication-bundle
  • 31.
    @stadolf @coding_earth devday.ioDEVDAYLOVESPHP Mock authentication /** * @When I authenticate using the email address :arg1 having the role :arg2 */ public function iAuthenticateAs($email, $role) { $user = $this->em->getRepository(User::class)->findOneBy(['email' => $email]); if (!$user) { $user = $this->userManager->createUser($email, $email); $user->setRoles([$role]); $this->em->persist($user); $this->em->flush(); } $jwtToken = $this->jwtManager->create($user); $this->request->setHttpHeader("Authorization", "Bearer $jwtToken"); } create JWT on the fly BehatchRestContext AuthenticationContext class AuthenticationContext extends RestContext
  • 32.
    @stadolf @coding_earth devday.ioDEVDAYLOVESPHP default: autoload: [ 'features/contexts' ] suites: default: paths: [ 'features' ] contexts: - AuthenticationContext: doctrine: "@doctrine" jwtManager: "@lexik_jwt_authentication.jwt_manager" userManager: "@AppSecurityUserManager" - FeatureContext: doctrine: "@doctrine" - ResourcefulContext: doctrine: "@doctrine" userManager: "@AppSecurityUserManager" - BehatMinkExtensionContextMinkContext
 - GraphqlContext - behatch:context:json: evaluationMode: javascript - behatch:context:system: root: “." - behatch:context:table - behatch:context:xml extensions: BehatSymfony2Extension: kernel: bootstrap: config/behat_bootstrap.php class: AppKernel BehatchExtension: ~ BehatMinkExtension: sessions: default: Integrate contexts “When I authenticate as “ “I send a "POST" request to“ “there is an organization…” use the Symfony Kernel & DI behat.yml
  • 33.
    @stadolf @coding_earth devday.ioDEVDAYLOVESPHP Integrate Contexts <?php final class GraphqlContext implements Context { /** @var BehatchContextRestContext */ private $restContext; private $request; public function __construct(Request $request) { $this->request = $request; } /** * Gives access to the Behatch context. * @BeforeScenario */ public function gatherContexts(BeforeScenarioScope $scope) { /** @var InitializedContextEnvironment $environment */ $environment = $scope->getEnvironment(); $this->restContext = $environment->getContext(AuthenticationContext::class); } /** @When I send the following GraphQL request: */ public function ISendTheFollowingGraphqlRequest(PyStringNode $request) { $this->IHaveTheFollowingGraphqlRequest($request); $this->request->setHttpHeader('Accept', null); $this->restContext->iSendARequestTo('GET', '/graphql?' . http_build_query($this->graphqlRequest)); } get REST context use REST context
  • 34.
    @stadolf @coding_earth devday.ioDEVDAYLOVESPHP @createSchema Scenario: query all products When I add "Content-Type" header equal to "application/ld+json" And I add "Accept" header equal to "application/ld+json" And I authenticate using the email address "user@organization.com" having the role "ROLE_USER" And I send the following GraphQL request: """ { products { edges { node { title width } } } } """ Then the response status code should be 200 And the response should be in JSON And the JSON node "data.products.edges[0].node.title" should be equal to "Ice Tea" And the JSON node "data.products.edges[0].node.width" should be equal to 300 Testing GraphQLhttp://behat.org/en/latest/ sensorevents.feature affects the REST Request
  • 35.
    @stadolf @coding_earth devday.ioDEVDAYLOVESPHP DEMO: BEHAT 
 FOR API-PLATFORM
  • 36.
    @stadolf @coding_earth devday.ioDEVDAYLOVESPHP Too long, didn’t listen ๏BDD is a great tool to define features in real language ๏Behat can be used for API testing ๏this way it’s closer to SpecBDD than StoryBDD ๏Don’t test everything like this - CI performance will kill you ๏it integrates nicely with api-platform ๏api-platform is great to build APIs.
  • 37.
    @stadolf @coding_earth devday.ioDEVDAYLOVESPHP That’s all, folks Stefan Adolf
 https://twitter.com/stadolf
 https://github.com/elmariachi111
 https://www.linkedin.com/in/stadolf/ @stadolf @coding_earth devday.io DEVDAYLOVESPHP
  • 38.