Extending Peridot


Customize your testing platform with events, plugins, reporters, scopes, and more!

Austin Morris / @austinsmorris
Slides: https://austinsmorris.github.io/extending-peridot

A Quick Primer

describe/it


          <?php

          describe('A thing', function () {
            // I am a suite!

            beforeEach(function () {
              // Somebody set up us the bomb!
            });

            it('should do a thing', function () {
              // I am a test!
            });

            // more it()...

            afterEach(function () {
              // Tear down!
            });
          });
        

Nested describes


          describe('A thing', function () {

            beforeEach(function () {
              // Somebody set up us the bomb!
            });

            context('in this context', function () {
              // I am another suite!

              beforeEach(function () {
                // I only run in this context
              });

              it('should do a very specific thing', function () {
                // Both beforeEach() functions were executed before me!
              });
            });
          });
        

Test execution:

  • There is a single global context (suite)
  • describe() and context() create suites
  • it() creates tests
  • Closure::bind() links tests (leaf) to suites (composite)
  • A test passes unless it throws an exception.

Peridot is a Symfony Console Application

"The Console component eases the creation of beautiful and testable command line interfaces."

Abstractions for:

  • CLI arguments
  • CLI options
  • CLI output

In the beginning, Peridot was without form and void...

peridot.php

Configure your PHP tests with PHP!

Even this is configurable:

$ vendor/bin/peridot -c foo/bar.php
$ vendor/bin/peridot --configuration foo/bar.php

peridot.php


          <?php

          use Evenement\EventEmitterInterface;

          /**
           * Configure peridot.
           *
           * @param EventEmitterInterface $eventEmitter
           */
          return function (EventEmitterInterface $eventEmitter) {
            // Everything that is cool and rad goes here!
          };
        

Events!

EventEmitter

Peridot has a single application event manager driving and providing access to the entire testing lifecyle.

Événement

          interface EventEmitterInterface
          {
            public function on($event, callable $listener);
            public function once($event, callable $listener);
            public function removeListener($event, callable $listener);
            public function removeAllListeners($event = null);
            public function listeners($event);
            public function emit($event, array $arguments = []);
          }
        

Peridot Events

  • 'peridot.start' - Define additional CLI args/options
  • 'peridot.configure' - Override default config
  • 'suite.start' - A suite starts
  • 'test.passed' - A test passed
  • 'test.failed' - A test failed
  • more...

peridot.php

Set the default path:


    use Evenement\EventEmitterInterface;
    use Peridot\Console\Environment;

    return function (EventEmitterInterface $eventEmitter) {
      $eventEmitter->on('peridot.start', function (Environment $environment) {
        $environment->getDefinition()->getArgument('path')->setDefault('specs');
      });
    };
        

Create your own events!


    use Evenement\EventEmitterInterface;
    use Peridot\Core\Test;

    return function (EventEmitterInterface $eventEmitter) {
      $eventEmitter->on('test.passed', function (Test $test) use ($eventEmitter) {
        $eventEmitter->emit('wackyEvent', [$test]);
      });

      $eventEmitter->on('wackyEvent', function (Test $test) {
        // do something wacky!
      });
    };
        

Plugins!

What is a Peridot plugin?

  • It's a thing that does a thing.
  • It makes Peridot do more that it can do without it.
  • It is added to the config file (peridot.php).
  • There is a "preferred" style.

MyPlugin


          class MyPlugin
          {
            protected $emitter;

            public function __construct(EventEmitterInterface $emitter)
            {
              $this->emitter = $emitter;
              $this->listen();
            }

            protected function listen()
            {
              $this->emitter->on('suite.start', [$this, 'onSuiteStart']);
            }

            public function onSuiteStart(Suite $suite)
            {
              // do something when a suite starts
            }
          }
        

Register your plugin:


          use Evenement\EventEmitterInterface;

          return function (EventEmitterInterface $eventEmitter) {
            new MyPlugin($eventEmitter);
          };
        

Peridot Yo Plugin


          use Evenement\EventEmitterInterface;
          use Peridot\Plugin\Yo\YoPlugin;

          return function (EventEmitterInterface $emitter) {
            $yoPlugin = YoPlugin::register(
              $emitter,
              "your-yo-token",
              ['USER1', 'USER2'],
              'http://linktobuild.com'
            );
            $yoPlugin->setBehavior(YoPlugin::BEHAVIOR_ON_FAIL);
          };
        

Peridot Watcher Plugin


          use Evenement\EventEmitterInterface;
          use Peridot\Plugin\Watcher\WatcherPlugin;

          return function(EventEmitterInterface $emitter) {
            $watcher = new WatcherPlugin($emitter);
            $watcher->track(__DIR__ . '/src');
          };
        

Peridot Concurrency Plugin


          use Evenement\EventEmitterInterface;
          use Peridot\Concurrency\ConcurrencyPlugin;

          return function (EventEmitterInterface $emitter) {
            new ConcurrencyPlugin($emitter);
          };
        

          $ vendor/bin/peridot --concurrent
        

Reporters!

Register with the 'peridot.reporters' event:


class DotReporterPlugin
{
  // ...

  protected function listen()
  {
    $this->emitter->on('peridot.reporters', [$this, 'onPeridotReporters']);
  }

  public function onPeridotReporters(InputInterface $input, ReporterFactory $reporters)
  {
    $reporters->register('dot', 'dot matrix', 'Peridot\Reporter\Dot\DotReporter');
  }
}
        

Listen to the test events:


      namespace Peridot\Reporter\Dot;

      use Peridot\Reporter\AbstractBaseReporter;

      class DotReporter extends AbstractBaseReporter
      {
        public function init()
        {
          $this->eventEmitter->on('test.passed', [$this, 'onTestPassed']);
          $this->eventEmitter->on('test.failed', [$this, 'onTestFailed']);
          $this->eventEmitter->on('test.pending', [$this, 'onTestPending']);
          $this->eventEmitter->on('runner.end', [$this, 'onRunnerEnd']);
        }
      }
        

Dot Reporter


          use Peridot\Reporter\Dot\DotReporterPlugin;

          return function(EventEmitterInterface $emitter) {
            new DotReporterPlugin($emitter);
          };
        

          $ vendor/bin/peridot -r dot
        

Code Coverage Reporters


        use Evenement\EventEmitterInterface;
        use Peridot\Reporter\CodeCoverageReporters;

        return function (EventEmitterInterface $eventEmitter) {
          (new CodeCoverageReporters($eventEmitter))->register();

          $eventEmitter->on('code-coverage.start', function ($reporter) {
            $reporter->addDirectoryToWhitelist(__DIR__ . '/src');
          });
        };
        

        $ vendor/bin/peridot -r html-code-coverage --code-coverage-path coverage/
        

Scopes!

Plugins can manipulate scope!


      class MyScopePlugin
      {
        // ...

        protected function listen()
        {
          $this->emitter->on('suite.start', [$this, 'onSuiteStart']);
        }

        public function onSuiteStart(Suite $suite)
        {
          $suite->getScope()->peridotAddChildScope(new MyScope());
        }
      }
        

Add things to $this:


          class MyScope
          {
            // tests can access $this->thingy
            public $thingy;

            // tests can call $this->doThingyMagic()
            public function doThingyMagic()
            {
              // magic happens!
            }
          }
        

Always available:


          use Evenement\EventEmitterInterface;

          return function (EventEmitterInterface $eventEmitter) {
            new MyScopePlugin($eventEmitter);
          };
        

Use on test by test basis:


          describe('A thing', function () {
            $scope = new MyScope();
            $this->peridotAddChildScope($scope);

            it('should do a thing', function() {
              $this->doThingyMagic()
            });
          }
        

Prophecy Plugin


          describe('A thing', function() {
            beforeEach(function () {
              // get a \Prophecy\Prophet
              $prophet = $this->getProphet()
            )};
          });
        

          describe('My\Thing', function() {
            it('should be a Thing', function() {
              assert($this->subject->reveal() instanceof Thing);
            });
          });
        

HttpKernel Plugin


      describe('My API', function() {
        describe('/my-route', function() {
          it('should return data', function() {
            $this->client->request('GET', '/my-route');
            $data = json_decode($this->client->getResponse()->getContent(), true);

            // assert($data blah blah blah...
          });
        });
      });
        

Doctrine Plugin


          use Evenement\EventEmitterInterface;
          use Peridot\Plugin\Doctrine\DoctrinePlugin;
          use Peridot\Plugin\Doctrine\EntityManager\EntityManagerService;

          return function (EventEmitterInterface $eventEmitter) {
            new DoctrinePlugin($eventEmitter, new EntityManagerService());
          };
        

          beforeEach(function () {
            // I can create an entity manager!
            $this->entityManager = $this->createEntityManager();
            $this->repository = new MyRepository($this->entityManager);
          });
        

Pro Tip 1:

Customize your entity manager creation


  use Doctrine\DBAL\Types\Type;

  Type::addType('datetimeutc', 'ASM\Doctrine\DBAL\Types\DateTimeUTCType');
  $factory = function () {
    // do something that creates an entity manager ..
    return $entityManager;
  };

  $service = (new EntityManagerService())->setEntityManagerFactory($factory);
  new DoctrinePlugin($eventEmitter, $service);
        

Pro Tip 2:

Copying is faster than creating


          use Nelmio\Alice\Fixtures;
          use Peridot\Plugin\Doctrine\EntityManager\SchemaService;

          // create the initial sqlite db used for testing
          $eventEmitter->on('runner.start', function () use ($factory) {
            $entityManager = $factory();
            $schemaService = new SchemaService($entityManager);
            $schemaService->dropDatabase()->createDatabase();

            Fixtures::load([__DIR__ . '/path/to/fixture.yml'], $entityManager);
            // load more fixtures...

            copy(__DIR__ . '/tmp/db.db', __DIR__ . '/tmp/clean.db');
          });

          // create a clean copy of the db before an entity manager is created
          $eventEmitter->on('doctrine.entityManager.preCreate', function () {
            copy(__DIR__ . '/tmp/clean.db', __DIR__ . '/tmp/db.db');
          });
        

More!

Leo!

An assertion library to complement Peridot.


          $ composer require peridot-php/leo:~1.0
        

Assert


          use Peridot\Leo\Interfaces\Assert;

          $assert = new Assert();
          $assert->typeOf('string', 'hello', 'is string');
          $assert->operator(5, '<', 6, 'should be less than');
          $assert->isResource(tempfile());
          $assert->throws(function() {
            throw new Exception("exception");
          }, 'Exception');
        

Expect


          expect([1,2,3])->to->have->length(3);
          expect($name)->to->be->a('string')
          expect($object)->to->have->property('name', 'brian');
          expect(function() {})->to->satisfy('is_callable');
          expect(5)->to->be->above(4)
          expect($num)->to->be->within(5, 10);
          expect('hello')->to->match('/^he/');
        

Extending Leo


          use Evenement\EventEmitterInterface;
          use Peridot\Leo\HttpFoundation\LeoHttpFoundation;
          use Peridot\Leo\Leo;

          return function (EventEmitterInterface $eventEmitter) {
            Leo::assertion()->extend(new LeoHttpFoundation());
          };
        

          expect($response)->to->allow(['POST', 'GET']);
          expect($response)->to->have->status(200);
          expect($response)->json->to->have->property('name');
        

tl;dr

  • Peridot is extensible.
  • Peridot is flexible.
  • Peridot is fun-ible.
  • Everything else is dead (and an anti-pattern).

Questions?