Decoupling with Design Patterns and Symfony DIC
@everzet · Spent more than 7 years writing so!ware · Spent more than 4 years learning businesses · Now filling the gaps between the two as a BDD Practice Manager @Inviqa
behat 3 promise #1 (of 2): extensibility
“Extensibility is a so!ware design principle defined as a system’s ability to have new functionality extended, in which the system’s internal structure and data flow are minimally or not affected”
“So!ware entities (classes, modules, functions, etc.) should be open for extension, but closed for modification”
behat 3 promise #2 (of 2): backwards compatibility
behat 3 - extensibility as the core concept - BC through extensibility
Symfony Bundles Behat extensions
Symfony Bundles & Behat extensions 1. Framework creates a temporary container 2. Framework asks the bundle to add its services 3. Framework merges all temporary containers 4. Framework compiles merged container
interface CompilerPassInterface { /** * You can modify the container here before it is dumped to PHP code. * * @param ContainerBuilder $container * * @api */ public function process(ContainerBuilder $container); }
class YourSuperBundle extends Bundle { public function build(ContainerBuilder $container) { parent::build($container); $container->addCompilerPass(new YourCompilerPass()); } }
v3.0 v1.0 (extensibility solution v1)
challenge: behat as the most extensible test framework
pattern: observer
class HookDispatcher extends DispatchingService implements EventSubscriberInterface { public static function getSubscribedEvents() { return array( EventInterface::BEFORE_SUITE => array('dispatchHooks', 10), EventInterface::AFTER_SUITE => array('dispatchHooks', 10), EventInterface::BEFORE_FEATURE => array('dispatchHooks', 10), ... ); } public function dispatchHooks(LifecycleEventInterface $event) { $hooksProvider = new HooksCarrierEvent($event->getSuite(), $event->getContextPool()); $this->dispatch(EventInterface::LOAD_HOOKS, $hooksProvider); foreach ($hooksProvider->getHooksForEvent($event) as $hook) { $this->dispatchHook($hook, $event); } } ... }
class HooksCarrierEvent extends Event implements LifecycleEventInterface { public function addHook(HookInterface $hook) { $this->hooks[] = $hook; } public function getHooksForEvent(Event $event) { return array_filter( $this->hooks, function ($hook) use ($event) { $eventName = $event->getName(); if ($eventName !== $hook->getEventName()) { return false; } return $hook; } ); } ... }
class DictionaryReader implements EventSubscriberInterface { public static function getSubscribedEvents() { return array( EventInterface::LOAD_HOOKS => array('loadHooks', 0), ... ); } public function loadHooks(HooksCarrierEvent $event) { foreach ($this->read($event->getSuite(), $event->getContextPool()) as $callback) { if ($callback instanceof HookInterface) { $event->addHook($callback); } } } ... }
extension point
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="..."> <services> <service id="event_dispatcher" class="SymfonyComponentEventDispatcherEventDispatcher"/> <service id="hook.hook_dispatcher" class="BehatBehatHookEventSubscriberHookDispatcher"> <argument type="service" id="event_dispatcher"/> <tag name="event_subscriber"/> </service> <service id="context.dictionary_reader" class="BehatBehatContextEventSubscriberDictionaryReader"> <tag name="event_subscriber"/> </service> </services> </container>
class EventSubscribersPass implements CompilerPassInterface { public function process(ContainerBuilder $container) { $dispatcherDefinition = $container->getDefinition('event_dispatcher'); foreach ($container->findTaggedServiceIds('event_subscriber') as $id => $attributes) { $dispatcherDefinition->addMethodCall('addSubscriber', array(new Reference($id))); } } }
where event dispatcher / observer is useful?
pub/sub as an architectural choice
“Coupling is a degree to which each program module relies on each one of the other modules”
“Cohesion is a degree to which the elements of a module belong together”
“Coupling is a degree to which each program module relies on each one of the other modules” public function dispatchHooks(LifecycleEventInterface $event) { $hooksProvider = new HooksCarrierEvent($event->getSuite(), $event->getContextPool()); $this->dispatch(EventInterface::LOAD_HOOKS, $hooksProvider); foreach ($hooksProvider->getHooksForEvent($event) as $hook) { $this->dispatchHook($hook, $event); } }
“Cohesion is a degree to which the elements of a module belong together” public function dispatchHooks(LifecycleEventInterface $event) { $hooksProvider = new HooksCarrierEvent($event->getSuite(), $event->getContextPool()); $this->dispatch(EventInterface::LOAD_HOOKS, $hooksProvider); foreach ($hooksProvider->getHooksForEvent($event) as $hook) { $this->dispatchHook($hook, $event); } }
Coupling ↓ Cohesion ↑
scratch that
v3.0 v2.0 (extensibility solution v2)
There is no single solution for extensibility. Because extensibility is not a single problem
framework extensions Since v2.5 behat has some very important extensions: 1. MinkExtension 2. Symfony2Extension
problem: there are multiple possible algorithms for a single responsibility
pattern: delegation loop
final class EnvironmentManager { private $handlers = array(); public function registerEnvironmentHandler(EnvironmentHandler $handler) { $this->handlers[] = $handler; } public function buildEnvironment(Suite $suite) { foreach ($this->handlers as $handler) { ... } } public function isolateEnvironment(Environment $environment, $testSubject = null) { foreach ($this->handlers as $handler) { ... } } }
interface EnvironmentHandler { public function supportsSuite(Suite $suite); public function buildEnvironment(Suite $suite); public function supportsEnvironmentAndSubject(Environment $environment, $testSubject = null); public function isolateEnvironment(Environment $environment, $testSubject = null); }
extension point
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="..."> <services> <service id=“environment.manager” class="BehatTestworkEnvironmentEnvironmentManager” /> <service id=“behat.context.environment.handler” class=“BehatBehatContextEnvironmentContextEnvironmentHandler”> <tag name=“environment.handler”/> </service> </services> </container>
final class EnvironmentHandlerPass implements CompilerPassInterface { public function process(ContainerBuilder $container) { $references = $this->processor->findAndSortTaggedServices($container, ‘environment.handler’); $definition = $container->getDefinition(‘environment.manager’); foreach ($references as $reference) { $definition->addMethodCall('registerEnvironmentHandler', array($reference)); } } }
where delegation loop is useful?
behat testers There are 5 testers in behat core: 1. FeatureTester 2. ScenarioTester 3. OutlineTester 4. BackgroundTester 5. StepTester
behat testers Behat needs to provide you with: · Hooks · Events
problem: we need to dynamically extend the core testers behaviour
pattern: decorator
final class RuntimeScenarioTester implements ScenarioTester { public function setUp(Environment $env, FeatureNode $feature, Scenario $example, $skip) { return new SuccessfulSetup(); } public function test(Environment $env, FeatureNode $feature, Scenario $scenario, $skip = false) { ... } public function tearDown(Environment $env, FeatureNode $feature, Scenario $scenario, $skip, TestResult $result) { return new SuccessfulTeardown(); } }
interface ScenarioTester { public function setUp(Environment $env, FeatureNode $feature, Scenario $scenario, $skip); public function test(Environment $env, FeatureNode $feature, Scenario $scenario, $skip); public function tearDown(Environment $env, FeatureNode $feature, Scenario $scenario, $skip, TestResult $result); }
final class EventDispatchingScenarioTester implements ScenarioTester { public function __construct(ScenarioTester $baseTester, EventDispatcherInterface $eventDispatcher) { $this->baseTester = $baseTester; $this->eventDispatcher = $eventDispatcher; } public function setUp(Environment $env, FeatureNode $feature, Scenario $scenario, $skip) { $event = new BeforeScenarioTested($env, $feature, $scenario); $this->eventDispatcher->dispatch($this->beforeEventName, $event); $setup = $this->baseTester->setUp($env, $feature, $scenario, $skip); return $setup; } public function test(Environment $env, FeatureNode $feature, Scenario $scenario, $skip) { return $this->baseTester->test($env, $feature, $scenario, $skip); } public function tearDown(Environment $env, FeatureNode $feature, Scenario $scenario, $skip, TestResult $result) { $teardown = $this->baseTester->tearDown($env, $feature, $scenario, $skip, $result); $event = new AfterScenarioTested($env, $feature, $scenario, $result, $teardown); $this->eventDispatcher->dispatch($event); return $teardown; } }
final class HookableScenarioTester implements ScenarioTester { public function __construct(ScenarioTester $baseTester, HookDispatcher $hookDispatcher) { $this->baseTester = $baseTester; $this->hookDispatcher = $hookDispatcher; } public function setUp(Environment $env, FeatureNode $feature, Scenario $example, $skip) { $setup = $this->baseTester->setUp($env, $feature, $scenario, $skip); $hookCallResults = $this->hookDispatcher->dispatchScopeHooks($setup); return new HookedSetup($setup, $hookCallResults); } public function test(Environment $env, FeatureNode $feature, Scenario $scenario, $skip = false) { return $this->baseTester->test($env, $feature, $scenario, $skip); } public function tearDown(Environment $env, FeatureNode $feature, Scenario $scenario, $skip, TestResult $result) { $teardown = $this->baseTester->tearDown($env, $feature, $scenario, $skip, $result); $hookCallResults = $this->hookDispatcher->dispatchScopeHooks($teardown); return new HookedTeardown($teardown, $hookCallResults); } }
extension point
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="..."> <services> <service id=“tester.scenario” class="BehatBehatTesterScenarioTester” /> <service id=“hooks.tester.scenario” class=“BehatBehatHooksTesterScenarioTester”> ... <tag name=“tester.scenario_wrapper” order=“100”/> </service> <service id=“events.tester.scenario” class=“BehatBehatEventsTesterScenarioTester”> ... <tag name=“tester.scenario_wrapper” order=“200”/> </service> </services> </container>
final class ScenarioTesterWrappersPass implements CompilerPassInterface { public function process(ContainerBuilder $container) { $references = $this->findAndReorderTaggedServices($container, ‘tester.scenario_wrapper’); foreach ($references as $reference) { $id = (string) $reference; $renamedId = $id . '.inner'; // This logic is based on SymfonyComponentDependencyInjectionCompilerDecoratorServicePass $definition = $container->getDefinition(‘tester.scenario’); $container->setDefinition($renamedId, $definition); $container->setAlias('tester.scenario', new Alias($id, $public)); $wrappingService = $container->getDefinition($id); $wrappingService->replaceArgument(0, new Reference($renamedId)); } } ... }
where decorator is useful?
behat output Behat has a very simple output:
behat output Until you start using backgrounds:
behat output And throwing exceptions from their hooks:
problem: we need to add behaviour to complex output logic
pattern: observer
pattern: chain of responsibility
pattern: composite
final class NodeEventListeningFormatter implements Formatter { public function __construct(EventListener $listener) { $this->listener = $listener; } public static function getSubscribedEvents() { return array(TestworkEventDispatcher::BEFORE_ALL_EVENTS => 'listenEvent'); } public function listenEvent(Event $event, $eventName = null) { $eventName = $eventName ?: $event->getName(); $this->listener->listenEvent($this, $event, $eventName); } }
final class ChainEventListener implements EventListener, Countable, IteratorAggregate { private $listeners; public function __construct(array $listeners) { $this->listeners = $listeners; } public function listenEvent(Formatter $formatter, Event $event, $eventName) { foreach ($this->listeners as $listener) { $listener->listenEvent($formatter, $event, $eventName); } } ... }
Event listeners Behat has 2 types of listeners: 1. Printers 2. Flow controllers
final class StepListener implements EventListener { public function listenEvent(Formatter $formatter, Event $event, $eventName) { $this->captureScenarioOnScenarioEvent($event); $this->forgetScenarioOnAfterEvent($eventName); $this->printStepSetupOnBeforeEvent($formatter, $event); $this->printStepOnAfterEvent($formatter, $event); } ... }
How do backgrounds work?
class FirstBackgroundFiresFirstListener implements EventListener { public function __construct(EventListener $descendant) { $this->descendant = $descendant; } public function listenEvent(Formatter $formatter, Event $event, $eventName) { $this->flushStatesIfBeginningOfTheFeature($eventName); $this->markFirstBackgroundPrintedAfterBackground($eventName); if ($this->isEventDelayedUntilFirstBackgroundPrinted($event)) { $this->delayedUntilBackgroundEnd[] = array($event, $eventName); return; } $this->descendant->listenEvent($formatter, $event, $eventName); $this->fireDelayedEventsOnAfterBackground($formatter, $eventName); } }
where composite and CoR are useful?
interface StepTester { public function setUp(Environment $env, FeatureNode $feature, StepNode $step, $skip); public function test(Environment $env, FeatureNode $feature, StepNode $step, $skip); public function tearDown(Environment $env, FeatureNode $feature, StepNode $step, $skip, StepResult $result); }
problem: we need to introduce backwards incompatible change into the API
pattern: adapter
interface ScenarioStepTester { public function setUp(Environment $env, FeatureNode $feature, ScenarioNode $scenario, StepNode $step, $skip); public function test(Environment $env, FeatureNode $feature, ScenarioNode $scenario, StepNode $step, $skip); public function tearDown(Environment $env, FeatureNode $feature, ScenarioNode $scenario, StepNode $step, $skip, StepResult $result); }
final class StepToScenarioTesterAdapter implements ScenarioStepTester { public function __construct(StepTester $stepTester) { ... } public function setUp(Environment $env, FeatureNode $feature, ScenarioNode $scenario, StepNode $step, $skip) { return $this->stepTester->setUp($env, $feature, $step, $skip); } public function test(Environment $env, FeatureNode $feature, ScenarioNode $scenario, StepNode $step, $skip) { return $this->stepTester->test($env, $feature, $step, $skip); } public function tearDown(Environment $env, FeatureNode $feature, ScenarioNode $scenario, StepNode $step, $skip, StepResult $result) { return $this->stepTester-> tearDown($env, $feature, $step, $skip); } }
final class StepTesterAdapterPass implements CompilerPassInterface { public function process(ContainerBuilder $container) { $references = $this->processor->findAndSortTaggedServices($container, ‘tester.step_wrapper’); foreach ($references as $reference) { $id = (string) $reference; $renamedId = $id . ‘.adaptee’; $adapteeDefinition = $container->getDefinition($id); $reflection = new ReflectionClass($adapteeDefinition->getClass()); if (!$reflection->implementsInterface(‘StepTester’)) { return; } $container->removeDefinition($id); $container->setDefinition( $id, new Definition(‘StepToScenarioTesterAdapter’, array( $adapteeDefinition )); ); } } }
where adapter is useful?
demo
backwards compatibility
backwards compatibility Backwards compatibility in Behat comes from the extensibility. 1. Everything is extension 2. New features are extensions too 3. New features could be toggled on/off
performance implications
performance implications · 2x more objects in v3 than in v2 · Value objects are used instead of simple types · A lot of additional concepts throughout · It must be slow
yet...
how?
how? immutability!
TestWork
TestWork
how?
Step1: Close the doors Assume you have no extension points by default. 1. Private properties 2. Final classes
Step 2: Open doors properly when you need them 1. Identify the need for extension points 2. Make extension points explicit
Private properties ...
Final classes
class BundleFeatureLocator extends FilesystemFeatureLocator { public function locateSpecifications(Suite $suite, $locator) { if (!$suite instanceof SymfonyBundleSuite) { return new noSpecificationsIterator($suite); } $bundle = $suite->getBundle(); if (0 !== strpos($locator, '@' . $bundle->getName())) { return new NoSpecificationsIterator($suite); } $locatorSuffix = substr($locator, strlen($bundle->getName()) + 1); return parent::locateSpecifications($suite, $bundle->getPath() . '/Features' . $locatorSuffix); } }
final class BundleFeatureLocator implements SpecificationLocator { public function __construct(SpecificationLocator $baseLocator) { ... } public function locateSpecifications(Suite $suite, $locator) { if (!$suite instanceof SymfonyBundleSuite) { return new noSpecificationsIterator($suite); } $bundle = $suite->getBundle(); if (0 !== strpos($locator, '@' . $bundle->getName())) { return new NoSpecificationsIterator($suite); } $locatorSuffix = substr($locator, strlen($bundle->getName()) + 1); return $this->baseLocator->locateSpecifications($suite, $bundle->getPath() . '/Features' . $locatorSuffix); } }
the most closed most extensible testing framework
ask questions close Feed! L♻♻ps: https://joind.in/11559

Decoupling with Design Patterns and Symfony2 DIC

  • 1.
    Decoupling with DesignPatterns and Symfony DIC
  • 2.
    @everzet · Spentmore than 7 years writing so!ware · Spent more than 4 years learning businesses · Now filling the gaps between the two as a BDD Practice Manager @Inviqa
  • 6.
    behat 3 promise#1 (of 2): extensibility
  • 7.
    “Extensibility is aso!ware design principle defined as a system’s ability to have new functionality extended, in which the system’s internal structure and data flow are minimally or not affected”
  • 8.
    “So!ware entities (classes,modules, functions, etc.) should be open for extension, but closed for modification”
  • 9.
    behat 3 promise#2 (of 2): backwards compatibility
  • 11.
    behat 3 -extensibility as the core concept - BC through extensibility
  • 12.
  • 13.
    Symfony Bundles &Behat extensions 1. Framework creates a temporary container 2. Framework asks the bundle to add its services 3. Framework merges all temporary containers 4. Framework compiles merged container
  • 14.
    interface CompilerPassInterface { /** * You can modify the container here before it is dumped to PHP code. * * @param ContainerBuilder $container * * @api */ public function process(ContainerBuilder $container); }
  • 15.
    class YourSuperBundle extendsBundle { public function build(ContainerBuilder $container) { parent::build($container); $container->addCompilerPass(new YourCompilerPass()); } }
  • 16.
  • 17.
    challenge: behat asthe most extensible test framework
  • 18.
  • 19.
    class HookDispatcher extendsDispatchingService implements EventSubscriberInterface { public static function getSubscribedEvents() { return array( EventInterface::BEFORE_SUITE => array('dispatchHooks', 10), EventInterface::AFTER_SUITE => array('dispatchHooks', 10), EventInterface::BEFORE_FEATURE => array('dispatchHooks', 10), ... ); } public function dispatchHooks(LifecycleEventInterface $event) { $hooksProvider = new HooksCarrierEvent($event->getSuite(), $event->getContextPool()); $this->dispatch(EventInterface::LOAD_HOOKS, $hooksProvider); foreach ($hooksProvider->getHooksForEvent($event) as $hook) { $this->dispatchHook($hook, $event); } } ... }
  • 20.
    class HooksCarrierEvent extendsEvent implements LifecycleEventInterface { public function addHook(HookInterface $hook) { $this->hooks[] = $hook; } public function getHooksForEvent(Event $event) { return array_filter( $this->hooks, function ($hook) use ($event) { $eventName = $event->getName(); if ($eventName !== $hook->getEventName()) { return false; } return $hook; } ); } ... }
  • 21.
    class DictionaryReader implementsEventSubscriberInterface { public static function getSubscribedEvents() { return array( EventInterface::LOAD_HOOKS => array('loadHooks', 0), ... ); } public function loadHooks(HooksCarrierEvent $event) { foreach ($this->read($event->getSuite(), $event->getContextPool()) as $callback) { if ($callback instanceof HookInterface) { $event->addHook($callback); } } } ... }
  • 22.
  • 23.
    <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="..."> <services> <service id="event_dispatcher" class="SymfonyComponentEventDispatcherEventDispatcher"/> <service id="hook.hook_dispatcher" class="BehatBehatHookEventSubscriberHookDispatcher"> <argument type="service" id="event_dispatcher"/> <tag name="event_subscriber"/> </service> <service id="context.dictionary_reader" class="BehatBehatContextEventSubscriberDictionaryReader"> <tag name="event_subscriber"/> </service> </services> </container>
  • 24.
    class EventSubscribersPass implementsCompilerPassInterface { public function process(ContainerBuilder $container) { $dispatcherDefinition = $container->getDefinition('event_dispatcher'); foreach ($container->findTaggedServiceIds('event_subscriber') as $id => $attributes) { $dispatcherDefinition->addMethodCall('addSubscriber', array(new Reference($id))); } } }
  • 25.
    where event dispatcher/ observer is useful?
  • 26.
    pub/sub as an architectural choice
  • 27.
    “Coupling is adegree to which each program module relies on each one of the other modules”
  • 28.
    “Cohesion is adegree to which the elements of a module belong together”
  • 29.
    “Coupling is adegree to which each program module relies on each one of the other modules” public function dispatchHooks(LifecycleEventInterface $event) { $hooksProvider = new HooksCarrierEvent($event->getSuite(), $event->getContextPool()); $this->dispatch(EventInterface::LOAD_HOOKS, $hooksProvider); foreach ($hooksProvider->getHooksForEvent($event) as $hook) { $this->dispatchHook($hook, $event); } }
  • 30.
    “Cohesion is adegree to which the elements of a module belong together” public function dispatchHooks(LifecycleEventInterface $event) { $hooksProvider = new HooksCarrierEvent($event->getSuite(), $event->getContextPool()); $this->dispatch(EventInterface::LOAD_HOOKS, $hooksProvider); foreach ($hooksProvider->getHooksForEvent($event) as $hook) { $this->dispatchHook($hook, $event); } }
  • 31.
  • 32.
  • 33.
  • 34.
    There is nosingle solution for extensibility. Because extensibility is not a single problem
  • 35.
    framework extensions Sincev2.5 behat has some very important extensions: 1. MinkExtension 2. Symfony2Extension
  • 36.
    problem: there aremultiple possible algorithms for a single responsibility
  • 37.
  • 38.
    final class EnvironmentManager { private $handlers = array(); public function registerEnvironmentHandler(EnvironmentHandler $handler) { $this->handlers[] = $handler; } public function buildEnvironment(Suite $suite) { foreach ($this->handlers as $handler) { ... } } public function isolateEnvironment(Environment $environment, $testSubject = null) { foreach ($this->handlers as $handler) { ... } } }
  • 39.
    interface EnvironmentHandler { public function supportsSuite(Suite $suite); public function buildEnvironment(Suite $suite); public function supportsEnvironmentAndSubject(Environment $environment, $testSubject = null); public function isolateEnvironment(Environment $environment, $testSubject = null); }
  • 40.
  • 41.
    <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="..."> <services> <service id=“environment.manager” class="BehatTestworkEnvironmentEnvironmentManager” /> <service id=“behat.context.environment.handler” class=“BehatBehatContextEnvironmentContextEnvironmentHandler”> <tag name=“environment.handler”/> </service> </services> </container>
  • 42.
    final class EnvironmentHandlerPassimplements CompilerPassInterface { public function process(ContainerBuilder $container) { $references = $this->processor->findAndSortTaggedServices($container, ‘environment.handler’); $definition = $container->getDefinition(‘environment.manager’); foreach ($references as $reference) { $definition->addMethodCall('registerEnvironmentHandler', array($reference)); } } }
  • 43.
  • 44.
    behat testers Thereare 5 testers in behat core: 1. FeatureTester 2. ScenarioTester 3. OutlineTester 4. BackgroundTester 5. StepTester
  • 45.
    behat testers Behatneeds to provide you with: · Hooks · Events
  • 46.
    problem: we needto dynamically extend the core testers behaviour
  • 47.
  • 48.
    final class RuntimeScenarioTesterimplements ScenarioTester { public function setUp(Environment $env, FeatureNode $feature, Scenario $example, $skip) { return new SuccessfulSetup(); } public function test(Environment $env, FeatureNode $feature, Scenario $scenario, $skip = false) { ... } public function tearDown(Environment $env, FeatureNode $feature, Scenario $scenario, $skip, TestResult $result) { return new SuccessfulTeardown(); } }
  • 49.
    interface ScenarioTester { public function setUp(Environment $env, FeatureNode $feature, Scenario $scenario, $skip); public function test(Environment $env, FeatureNode $feature, Scenario $scenario, $skip); public function tearDown(Environment $env, FeatureNode $feature, Scenario $scenario, $skip, TestResult $result); }
  • 50.
    final class EventDispatchingScenarioTesterimplements ScenarioTester { public function __construct(ScenarioTester $baseTester, EventDispatcherInterface $eventDispatcher) { $this->baseTester = $baseTester; $this->eventDispatcher = $eventDispatcher; } public function setUp(Environment $env, FeatureNode $feature, Scenario $scenario, $skip) { $event = new BeforeScenarioTested($env, $feature, $scenario); $this->eventDispatcher->dispatch($this->beforeEventName, $event); $setup = $this->baseTester->setUp($env, $feature, $scenario, $skip); return $setup; } public function test(Environment $env, FeatureNode $feature, Scenario $scenario, $skip) { return $this->baseTester->test($env, $feature, $scenario, $skip); } public function tearDown(Environment $env, FeatureNode $feature, Scenario $scenario, $skip, TestResult $result) { $teardown = $this->baseTester->tearDown($env, $feature, $scenario, $skip, $result); $event = new AfterScenarioTested($env, $feature, $scenario, $result, $teardown); $this->eventDispatcher->dispatch($event); return $teardown; } }
  • 51.
    final class HookableScenarioTesterimplements ScenarioTester { public function __construct(ScenarioTester $baseTester, HookDispatcher $hookDispatcher) { $this->baseTester = $baseTester; $this->hookDispatcher = $hookDispatcher; } public function setUp(Environment $env, FeatureNode $feature, Scenario $example, $skip) { $setup = $this->baseTester->setUp($env, $feature, $scenario, $skip); $hookCallResults = $this->hookDispatcher->dispatchScopeHooks($setup); return new HookedSetup($setup, $hookCallResults); } public function test(Environment $env, FeatureNode $feature, Scenario $scenario, $skip = false) { return $this->baseTester->test($env, $feature, $scenario, $skip); } public function tearDown(Environment $env, FeatureNode $feature, Scenario $scenario, $skip, TestResult $result) { $teardown = $this->baseTester->tearDown($env, $feature, $scenario, $skip, $result); $hookCallResults = $this->hookDispatcher->dispatchScopeHooks($teardown); return new HookedTeardown($teardown, $hookCallResults); } }
  • 52.
  • 53.
    <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="..."> <services> <service id=“tester.scenario” class="BehatBehatTesterScenarioTester” /> <service id=“hooks.tester.scenario” class=“BehatBehatHooksTesterScenarioTester”> ... <tag name=“tester.scenario_wrapper” order=“100”/> </service> <service id=“events.tester.scenario” class=“BehatBehatEventsTesterScenarioTester”> ... <tag name=“tester.scenario_wrapper” order=“200”/> </service> </services> </container>
  • 54.
    final class ScenarioTesterWrappersPassimplements CompilerPassInterface { public function process(ContainerBuilder $container) { $references = $this->findAndReorderTaggedServices($container, ‘tester.scenario_wrapper’); foreach ($references as $reference) { $id = (string) $reference; $renamedId = $id . '.inner'; // This logic is based on SymfonyComponentDependencyInjectionCompilerDecoratorServicePass $definition = $container->getDefinition(‘tester.scenario’); $container->setDefinition($renamedId, $definition); $container->setAlias('tester.scenario', new Alias($id, $public)); $wrappingService = $container->getDefinition($id); $wrappingService->replaceArgument(0, new Reference($renamedId)); } } ... }
  • 55.
  • 56.
    behat output Behathas a very simple output:
  • 57.
    behat output Untilyou start using backgrounds:
  • 58.
    behat output Andthrowing exceptions from their hooks:
  • 59.
    problem: we needto add behaviour to complex output logic
  • 60.
  • 61.
    pattern: chain ofresponsibility
  • 62.
  • 63.
    final class NodeEventListeningFormatterimplements Formatter { public function __construct(EventListener $listener) { $this->listener = $listener; } public static function getSubscribedEvents() { return array(TestworkEventDispatcher::BEFORE_ALL_EVENTS => 'listenEvent'); } public function listenEvent(Event $event, $eventName = null) { $eventName = $eventName ?: $event->getName(); $this->listener->listenEvent($this, $event, $eventName); } }
  • 64.
    final class ChainEventListenerimplements EventListener, Countable, IteratorAggregate { private $listeners; public function __construct(array $listeners) { $this->listeners = $listeners; } public function listenEvent(Formatter $formatter, Event $event, $eventName) { foreach ($this->listeners as $listener) { $listener->listenEvent($formatter, $event, $eventName); } } ... }
  • 65.
    Event listeners Behathas 2 types of listeners: 1. Printers 2. Flow controllers
  • 66.
    final class StepListenerimplements EventListener { public function listenEvent(Formatter $formatter, Event $event, $eventName) { $this->captureScenarioOnScenarioEvent($event); $this->forgetScenarioOnAfterEvent($eventName); $this->printStepSetupOnBeforeEvent($formatter, $event); $this->printStepOnAfterEvent($formatter, $event); } ... }
  • 67.
  • 68.
    class FirstBackgroundFiresFirstListener implementsEventListener { public function __construct(EventListener $descendant) { $this->descendant = $descendant; } public function listenEvent(Formatter $formatter, Event $event, $eventName) { $this->flushStatesIfBeginningOfTheFeature($eventName); $this->markFirstBackgroundPrintedAfterBackground($eventName); if ($this->isEventDelayedUntilFirstBackgroundPrinted($event)) { $this->delayedUntilBackgroundEnd[] = array($event, $eventName); return; } $this->descendant->listenEvent($formatter, $event, $eventName); $this->fireDelayedEventsOnAfterBackground($formatter, $eventName); } }
  • 69.
    where composite andCoR are useful?
  • 71.
    interface StepTester { public function setUp(Environment $env, FeatureNode $feature, StepNode $step, $skip); public function test(Environment $env, FeatureNode $feature, StepNode $step, $skip); public function tearDown(Environment $env, FeatureNode $feature, StepNode $step, $skip, StepResult $result); }
  • 72.
    problem: we needto introduce backwards incompatible change into the API
  • 73.
  • 74.
    interface ScenarioStepTester { public function setUp(Environment $env, FeatureNode $feature, ScenarioNode $scenario, StepNode $step, $skip); public function test(Environment $env, FeatureNode $feature, ScenarioNode $scenario, StepNode $step, $skip); public function tearDown(Environment $env, FeatureNode $feature, ScenarioNode $scenario, StepNode $step, $skip, StepResult $result); }
  • 75.
    final class StepToScenarioTesterAdapterimplements ScenarioStepTester { public function __construct(StepTester $stepTester) { ... } public function setUp(Environment $env, FeatureNode $feature, ScenarioNode $scenario, StepNode $step, $skip) { return $this->stepTester->setUp($env, $feature, $step, $skip); } public function test(Environment $env, FeatureNode $feature, ScenarioNode $scenario, StepNode $step, $skip) { return $this->stepTester->test($env, $feature, $step, $skip); } public function tearDown(Environment $env, FeatureNode $feature, ScenarioNode $scenario, StepNode $step, $skip, StepResult $result) { return $this->stepTester-> tearDown($env, $feature, $step, $skip); } }
  • 76.
    final class StepTesterAdapterPassimplements CompilerPassInterface { public function process(ContainerBuilder $container) { $references = $this->processor->findAndSortTaggedServices($container, ‘tester.step_wrapper’); foreach ($references as $reference) { $id = (string) $reference; $renamedId = $id . ‘.adaptee’; $adapteeDefinition = $container->getDefinition($id); $reflection = new ReflectionClass($adapteeDefinition->getClass()); if (!$reflection->implementsInterface(‘StepTester’)) { return; } $container->removeDefinition($id); $container->setDefinition( $id, new Definition(‘StepToScenarioTesterAdapter’, array( $adapteeDefinition )); ); } } }
  • 77.
  • 78.
  • 79.
  • 80.
    backwards compatibility Backwardscompatibility in Behat comes from the extensibility. 1. Everything is extension 2. New features are extensions too 3. New features could be toggled on/off
  • 81.
  • 82.
    performance implications ·2x more objects in v3 than in v2 · Value objects are used instead of simple types · A lot of additional concepts throughout · It must be slow
  • 83.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
    Step1: Close thedoors Assume you have no extension points by default. 1. Private properties 2. Final classes
  • 91.
    Step 2: Opendoors properly when you need them 1. Identify the need for extension points 2. Make extension points explicit
  • 92.
  • 93.
  • 94.
    class BundleFeatureLocator extendsFilesystemFeatureLocator { public function locateSpecifications(Suite $suite, $locator) { if (!$suite instanceof SymfonyBundleSuite) { return new noSpecificationsIterator($suite); } $bundle = $suite->getBundle(); if (0 !== strpos($locator, '@' . $bundle->getName())) { return new NoSpecificationsIterator($suite); } $locatorSuffix = substr($locator, strlen($bundle->getName()) + 1); return parent::locateSpecifications($suite, $bundle->getPath() . '/Features' . $locatorSuffix); } }
  • 95.
    final class BundleFeatureLocatorimplements SpecificationLocator { public function __construct(SpecificationLocator $baseLocator) { ... } public function locateSpecifications(Suite $suite, $locator) { if (!$suite instanceof SymfonyBundleSuite) { return new noSpecificationsIterator($suite); } $bundle = $suite->getBundle(); if (0 !== strpos($locator, '@' . $bundle->getName())) { return new NoSpecificationsIterator($suite); } $locatorSuffix = substr($locator, strlen($bundle->getName()) + 1); return $this->baseLocator->locateSpecifications($suite, $bundle->getPath() . '/Features' . $locatorSuffix); } }
  • 96.
    the most closedmost extensible testing framework
  • 97.
    ask questions closeFeed! L♻♻ps: https://joind.in/11559