BDD test framework style implemented using Free Monad
authorwidmogrod <widmogrod@gmail.com>
Wed, 13 Dec 2017 00:10:04 +0000 (01:10 +0100)
committerwidmogrod <widmogrod@gmail.com>
Wed, 13 Dec 2017 00:10:04 +0000 (01:10 +0100)
example/Free2BddStyleDSLTest.php [new file with mode: 0644]

diff --git a/example/Free2BddStyleDSLTest.php b/example/Free2BddStyleDSLTest.php
new file mode 100644 (file)
index 0000000..c1507c2
--- /dev/null
@@ -0,0 +1,270 @@
+<?php
+
+namespace example;
+
+use Widmogrod\FantasyLand\Functor;
+use Widmogrod\Monad\Free2\MonadFree;
+use Widmogrod\Monad\Free2\Pure;
+use Widmogrod\Monad\State;
+use const Widmogrod\Monad\State\value;
+use function Widmogrod\Functional\curryN;
+use function Widmogrod\Functional\match;
+use function Widmogrod\Functional\push;
+use function Widmogrod\Monad\Free2\foldFree;
+use function Widmogrod\Monad\Free2\liftF;
+
+interface ScenarioF extends Functor
+{
+}
+
+class Given implements ScenarioF
+{
+    public $desc;
+    public $state;
+    public $next;
+
+    public function __construct(string $desc, $state, MonadFree $next)
+    {
+        $this->desc = $desc;
+        $this->state = $state;
+        $this->next = $next;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function map(callable $function)
+    {
+        return new self(
+            $this->desc,
+            $this->state,
+            $function($this->next)
+        );
+    }
+}
+
+class When implements ScenarioF
+{
+    public $action;
+    public $next;
+
+    public function __construct(string $action, MonadFree $next)
+    {
+        $this->action = $action;
+        $this->next = $next;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function map(callable $function)
+    {
+        return new self(
+            $this->action,
+            $function($this->next)
+        );
+    }
+}
+
+class Then implements ScenarioF
+{
+    public $assertion;
+    public $next;
+
+    public function __construct(string $assertion, MonadFree $next)
+    {
+        $this->assertion = $assertion;
+        $this->next = $next;
+    }
+
+    /**
+     * @inheritdoc
+     */
+    public function map(callable $function)
+    {
+        return new self(
+            $this->assertion,
+            $function($this->next)
+        );
+    }
+}
+
+function scenario(string $desc, $state): Scenario
+{
+    return new Scenario(given_($desc, $state));
+}
+
+function given_(string $desc, $state): MonadFree
+{
+    return liftF(new Given($desc, $state, Pure::of('-given-')));
+}
+
+function when_(string $action): MonadFree
+{
+    return liftF(new When($action, Pure::of('-when-')));
+}
+
+function then_(string $assertion): MonadFree
+{
+    return liftF(new Then($assertion, Pure::of('-then-')));
+}
+
+
+class Scenario
+{
+    private $free;
+
+    public function __construct(MonadFree $free)
+    {
+        $this->free = $free;
+    }
+
+    public function When(string $action): self
+    {
+        return new self($this->free->bind(function () use ($action) {
+            return when_($action);
+        }));
+    }
+
+    public function Then(string $assertion): self
+    {
+        return new self($this->free->bind(function () use ($assertion) {
+            return then_($assertion);
+        }));
+    }
+
+    public function Run(array $when, array $then)
+    {
+        $interpretAction = curryN(3, interpretAction);
+        $interpretAssertion = curryN(3, interpretAssertion);
+        $interpretScenario = curryN(3, interpretScenario);
+
+        $interpret = $interpretScenario($interpretAction($when), $interpretAssertion($then));
+
+        $state = foldFree($interpret, $this->free, value);
+        $result = State\execState($state, []);
+
+        return $result;
+    }
+}
+
+function Given(string $desc, $state): Scenario
+{
+    return new Scenario(given_($desc, $state));
+}
+
+const interpretScenario = 'example\interpretScenario';
+
+/**
+ * interpretScenario :: (a -> b) -> (a -> Bool) -> ScenarioF -> State MonadFree b
+ */
+function interpretScenario(callable $interpretAction, callable $interpretAssertion, ScenarioF $f)
+{
+    return match([
+        Given::class => function (Given $a): State {
+            return State::of(function () use ($a) {
+                return [$a->next, $a->state];
+            });
+        },
+        When::class => function (When $a) use ($interpretAction): State {
+            return State::of(function ($state) use ($interpretAction, $a) {
+                $state = $interpretAction($a->action, $state);
+
+                return [$a->next, $state];
+            });
+        },
+        Then::class => function (Then $a) use ($interpretAssertion): State {
+            return State::of(function ($state) use ($interpretAssertion, $a) {
+                $ok = $interpretAssertion($a->assertion, $state);
+                assert($ok, $a->assertion);
+
+                return [$a->next, $state];
+            });
+        },
+    ], $f);
+}
+
+const interpretAction = 'example\interpretAction';
+
+/**
+ * interpretAction :: List (String -> (a -> a)) -> String -> a -> a
+ */
+function interpretAction(array $patterns, string $s, $state)
+{
+    return matchRegexp(wrapWithState($patterns, $state), $s);
+}
+
+const interpretAssertion = 'example\interpretAssertion';
+
+/**
+ * interpretAssertion :: List (String -> (a -> Bool)) -> String -> a -> Bool
+ */
+function interpretAssertion($patterns, string $s, $state): bool
+{
+    return matchRegexp(wrapWithState($patterns, $state), $s);
+}
+
+function wrapWithState(array $patterns, $state)
+{
+    return array_map(function (callable $fn) use ($state) {
+        return function () use ($fn, $state) {
+            $args = push([$state], func_get_args());
+
+            return call_user_func_array($fn, $args);
+        };
+    }, $patterns);
+}
+
+function matchRegexp(array $patterns, $value = null)
+{
+    return call_user_func_array(curryN(2, function (array $patterns, $value) {
+        foreach ($patterns as $pattern => $fn) {
+            if (false !== preg_match($pattern, $value, $matches)) {
+                return call_user_func_array($fn, array_slice($matches, 1));
+            }
+        }
+
+        throw new \Exception(sprintf(
+            'Cannot match "%s" to list of regexp %s',
+            $value,
+            implode(', ', array_keys($patterns))
+        ));
+    }), func_get_args());
+}
+
+class Free2BddStyleDSLTest extends \PHPUnit_Framework_TestCase
+{
+    public function test_it_should_interpret_bdd_scenario()
+    {
+        $state = [
+            'productsCount' => 0,
+            'products' => [],
+        ];
+
+        $scenario =
+            Given('Product in cart', $state)
+                ->When("I add product 'coca-cola'")
+                ->When("I add product 'milk'")
+                ->Then("The number of products is '2'");
+
+        $result = $scenario->Run([
+            "/^I add product '(.*)'/" => function ($state, $productName) {
+                $state['productsCount'] += 1;
+                $state['products'][] = $productName;
+
+                return $state;
+            },
+        ], [
+            "/^The number of products is '(\d+)'/" => function ($state, int $expected) {
+                return $state['productsCount'] === $expected;
+            },
+        ]);
+
+        $this->assertInternalType('array', $result);
+        $this->assertArrayHasKey('productsCount', $result);
+        $this->assertArrayHasKey('products', $result);
+
+        $this->assertEquals(2, $result['productsCount']);
+        $this->assertEquals(['coca-cola', 'milk'], $result['products']);
+    }
+}