Skip to content
This repository has been archived by the owner on Jul 29, 2022. It is now read-only.

Commit

Permalink
[RFR] Inject store arguments in Behat hooks non step methods (#24)
Browse files Browse the repository at this point in the history
* Inject store arguments in Behat hooks non step methods

* Reorganize project structure

* Try to decorate Behat services for Hook override

* Successfully decorates HookableScenarioTester

* Manage AfterScenario events

* Fix AfterScenario feature

* Add unit tests

* Apply PHP-CS fixer

* Update documentation

* Fix review comments
  • Loading branch information
vincentchalamon authored and Rodrigue Villetard committed Apr 26, 2017
1 parent 7e7a730 commit d8bfad9
Show file tree
Hide file tree
Showing 14 changed files with 525 additions and 30 deletions.
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,47 @@ public function giveBananaToGorilla($monkey, $scenarioBanana, Bonobo $bonobo)
}
```

### Using state fragments in Behat hook methods

It's also possible to consume state fragments in hook methods: `BeforeScenario` & `AfterScenario`. And much better,
the order is not important, you can set your arguments in any order you want:

```php
use Behat\Behat\Hook\Scope\AfterScenarioScope;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Gorghoa\ScenarioStateBehatExtension\Annotation\ScenarioStateArgument;

/**
* @BeforeScenario
*
* @ScenarioStateArgument("scenarioBanana")
*
* @param string $scenarioBanana
* @param BeforeScenarioScope $scope
*/
public function checkBananaBeforeScenario($scenarioBanana, BeforeScenarioScope $scope)
{
// (note that PHPUnit is here only given as an example, feel free to use any asserter you want)
\PHPUnit_Framework_Assert::assertEquals($scenarioBanana, 'Yammy Banana');
\PHPUnit_Framework_Assert::assertNotNull($scope);
}

/**
* @AfterScenario
*
* @ScenarioStateArgument("scenarioBanana")
*
* @param string $scenarioBanana
* @param AfterScenarioScope $scope
*/
public function checkBananaAfterScenario($scenarioBanana, AfterScenarioScope $scope)
{
// (note that PHPUnit is here only given as an example, feel free to use any asserter you want)
\PHPUnit_Framework_Assert::assertEquals($scenarioBanana, 'Yammy Banana');
\PHPUnit_Framework_Assert::assertNotNull($scope);
}
```

## Why injecting state's fragments through method params

1. Clear dependencies declaration for the step method
Expand Down
19 changes: 8 additions & 11 deletions features/scenario_state.feature
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
Feature: Scenario shared state
In order to statefully test a
suite of calls to a stateless
system, I need a way to store the
scenario state
In order to statefully test a suite of calls to a stateless system, I need a way to store the scenario state

Scenario: Scenario state should be shared between steps
When I run "behat --no-colors features/monkey.feature"
Then it should pass with:
"""
1 scenario (1 passed)
"""
"""
1 scenario (1 passed)
"""

Scenario: Scenario state should be reseted between scenarios
Scenario: Scenario state should be reset between scenarios
When I run "behat --no-colors features/donkeys.feature"
Then it should fail with:
"""
[Behat\Testwork\Argument\Exception\UnknownParameterValueException]
"""
"""
[Behat\Testwork\Argument\Exception\UnknownParameterValueException]
"""
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* file that was distributed with this source code.
*/

namespace Gorghoa\ScenarioStateBehatExtension;
namespace Gorghoa\ScenarioStateBehatExtension\Argument;

use Behat\Testwork\Argument\ArgumentOrganiser;
use Doctrine\Common\Annotations\Reader;
Expand Down Expand Up @@ -51,7 +51,7 @@ public function __construct(ArgumentOrganiser $organiser, ScenarioStateInitializ
public function organiseArguments(ReflectionFunctionAbstract $function, array $match)
{
$i = array_slice(array_keys($match), -1, 1)[0];
$paramsKeys = array_map(function($element) {
$paramsKeys = array_map(function ($element) {
return $element->name;
}, $function->getParameters());

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php

/*
* This file is part of the ScenarioStateBehatExtension project.
*
Expand All @@ -7,7 +8,8 @@
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Gorghoa\ScenarioStateBehatExtension\ScenarioState\Exception;

namespace Gorghoa\ScenarioStateBehatExtension\Exception;

/**
* @author Walter Dolce <walterdolce@gmail.com>
Expand Down
117 changes: 117 additions & 0 deletions src/Hook/Dispatcher/ScenarioStateHookDispatcher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

/*
* This file is part of the ScenarioStateBehatExtension project.
*
* (c) Rodrigue Villetard <rodrigue.villetard@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Gorghoa\ScenarioStateBehatExtension\Hook\Dispatcher;

use Behat\Testwork\Call\CallCenter;
use Behat\Testwork\Call\CallResults;
use Behat\Testwork\Environment\Call\EnvironmentCall;
use Behat\Testwork\Hook\Call\HookCall;
use Behat\Testwork\Hook\HookRepository;
use Behat\Testwork\Hook\Scope\HookScope;
use Doctrine\Common\Annotations\Reader;
use Gorghoa\ScenarioStateBehatExtension\Annotation\ScenarioStateArgument;
use Gorghoa\ScenarioStateBehatExtension\Context\Initializer\ScenarioStateInitializer;

/**
* @author Vincent Chalamon <vincent@les-tilleuls.coop>
*/
class ScenarioStateHookDispatcher
{
/**
* @var HookRepository
*/
private $repository;

/**
* @var CallCenter
*/
private $callCenter;

/**
* @var Reader
*/
private $reader;

/**
* @var ScenarioStateInitializer
*/
private $store;

/**
* Initializes scenario state hook dispatcher.
*
* @param HookRepository $repository
* @param CallCenter $callCenter
* @param ScenarioStateInitializer $store
* @param Reader $reader
*/
public function __construct(HookRepository $repository, CallCenter $callCenter, ScenarioStateInitializer $store, Reader $reader)
{
$this->repository = $repository;
$this->callCenter = $callCenter;
$this->reader = $reader;
$this->store = $store;
}

/**
* Dispatches hooks for a specified event.
*
* @param HookScope $scope
*
* @return CallResults
*/
public function dispatchScopeHooks(HookScope $scope)
{
$results = [];
foreach ($this->repository->getScopeHooks($scope) as $hook) {
/** @var \ReflectionMethod $function */
$function = $hook->getReflection();

// No `@ScenarioStateArgument` annotation found
if (null === $this->reader->getMethodAnnotation($function, ScenarioStateArgument::class)) {
$results[] = $this->callCenter->makeCall(new HookCall($scope, $hook));
continue;
}

$paramsKeys = array_map(function ($element) {
return $element->name;
}, $function->getParameters());
$store = $this->store->getStore();
$params = $arguments = [];

// Prepare arguments from annotations
/** @var ScenarioStateArgument[] $annotations */
$annotations = $this->reader->getMethodAnnotations($function);
foreach ($annotations as $annotation) {
if ($annotation instanceof ScenarioStateArgument &&
in_array($annotation->getArgument(), $paramsKeys) &&
$store->hasStateFragment($annotation->getName())
) {
$params[$annotation->getArgument()] = $store->getStateFragment($annotation->getName());
}
}

// Manage `scope` argument
foreach ($function->getParameters() as $parameter) {
if (null !== $parameter->getClass() && get_class($scope) === $parameter->getClass()->getName()) {
$arguments[$parameter->getName()] = $scope;
} elseif (isset($params[$parameter->getName()])) {
$arguments[$parameter->getName()] = $params[$parameter->getName()];
}
}

$results[] = $this->callCenter->makeCall(new EnvironmentCall($scope->getEnvironment(), $hook, $arguments));
}

return new CallResults($results);
}
}
85 changes: 85 additions & 0 deletions src/Hook/Tester/ScenarioStateHookableScenarioTester.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

/*
* This file is part of the ScenarioStateBehatExtension project.
*
* (c) Rodrigue Villetard <rodrigue.villetard@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Gorghoa\ScenarioStateBehatExtension\Hook\Tester;

use Behat\Behat\Hook\Scope\AfterScenarioScope;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Behat\Tester\ScenarioTester;
use Behat\Gherkin\Node\FeatureNode;
use Behat\Gherkin\Node\ScenarioInterface as Scenario;
use Behat\Testwork\Environment\Environment;
use Behat\Testwork\Hook\Tester\Setup\HookedSetup;
use Behat\Testwork\Hook\Tester\Setup\HookedTeardown;
use Behat\Testwork\Tester\Result\TestResult;
use Gorghoa\ScenarioStateBehatExtension\Hook\Dispatcher\ScenarioStateHookDispatcher;

/**
* @author Vincent Chalamon <vincent@les-tilleuls.coop>
*/
final class ScenarioStateHookableScenarioTester implements ScenarioTester
{
/**
* @var ScenarioTester
*/
private $baseTester;

/**
* @var ScenarioStateHookDispatcher
*/
private $dispatcher;

/**
* @param ScenarioTester $baseTester
* @param ScenarioStateHookDispatcher $dispatcher
*/
public function __construct(ScenarioTester $baseTester, ScenarioStateHookDispatcher $dispatcher)
{
$this->baseTester = $baseTester;
$this->dispatcher = $dispatcher;
}

/**
* {@inheritdoc}
*/
public function setUp(Environment $env, FeatureNode $feature, Scenario $scenario, $skip)
{
$setup = $this->baseTester->setUp($env, $feature, $scenario, true);

if ($skip) {
return $setup;
}

return new HookedSetup($setup, $this->dispatcher->dispatchScopeHooks(new BeforeScenarioScope($env, $feature, $scenario)));
}

/**
* {@inheritdoc}
*/
public function test(Environment $env, FeatureNode $feature, Scenario $scenario, $skip)
{
return $this->baseTester->test($env, $feature, $scenario, $skip);
}

/**
* {@inheritdoc}
*/
public function tearDown(Environment $env, FeatureNode $feature, Scenario $scenario, $skip, TestResult $result)
{
$teardown = $this->baseTester->tearDown($env, $feature, $scenario, true, $result);

if ($skip) {
return $teardown;
}

return new HookedTeardown($teardown, $this->dispatcher->dispatchScopeHooks(new AfterScenarioScope($env, $feature, $scenario, $result)));
}
}
2 changes: 1 addition & 1 deletion src/ScenarioState.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

namespace Gorghoa\ScenarioStateBehatExtension;

use Gorghoa\ScenarioStateBehatExtension\ScenarioState\Exception\MissingStateException;
use Gorghoa\ScenarioStateBehatExtension\Exception\MissingStateException;

/**
* @author Rodrigue Villetard <rodrigue.villetard@gmail.com>
Expand Down
3 changes: 2 additions & 1 deletion src/ScenarioStateInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
*/

namespace Gorghoa\ScenarioStateBehatExtension;
use Gorghoa\ScenarioStateBehatExtension\ScenarioState\Exception\MissingStateException;

use Gorghoa\ScenarioStateBehatExtension\Exception\MissingStateException;

/**
* @author Rodrigue Villetard <rodrigue.villetard@gmail.com>
Expand Down
Loading

0 comments on commit d8bfad9

Please sign in to comment.