atoum, introduction and discovering

PHPLimburg meetup

2018-11-21

First

slides will be available after the talk ;)

Who am I?

Grummfy

Jonathan Van Belle

@Grummfy

@Grummfy@mamot.fr

me@grummfy.be

github.com/grummfy

Monizze

 

Open source contributor (dev & doc, idea): atoum, hoa, ...

Unit test in PHP

PHPUnit

atoum

SimpleTest

(codeception)

Peridot

StoryPlayer

Lime

Kahlan

atoum

A simple, modern and intuitive unit testing framework

  • Tests isolation and parallelization
    • better performances,
    • avoid side-effects on tests,
  • Full-featured mock engine,
  • Virtual streams to mock filesystem,
  • And many more!

First test

<?php

namespace A\B {
  class C {
    public function iMReturningABool() {
      return false;
    }}}

namespace A\test\unit\B {
  class C extends \atoum {
    public function testIMReturningABool() {
      $this->newTestedInstance();
      $this->boolean($this->testedInstance->iMReturningABool())->isFalse;
      // or
      $this
        ->given($this->newTestedInstance())
          ->assert('Will return false')
            ->boolean($this->testedInstance->iMReturningABool())
              ->isFalse;
    }}}

Structural keywords

  • $this->given(/*...*/);
  • $this->let(/*...*/);
  • $this->define(/*...*/);
  • $this->if(/*...*/);
  • $this->and(/*...*/);
  • $this->when(/*...*/);
  • $this->then(/*...*/);

=> Storytelling style!

test is pleasant, readable, understandable

  • .atoum.php
    • run once
  • .bootstrap.atoum.php
    • run before each test run
  • inherited from parent directory
  • .autoloader.atoum.php
    • useless if you use composer
  • overwritten by cli args

Configuration

Configuration: debug

If you got some troubles, use ++verbose

vendor/bin/atoum ++verbose

Using ++verbose CLI argument…
Using '/var/www/src/.atoum.php' configuration file…
Using '/var/www/src/vendor/autoload.php' autoloader file…
Using '/tmp/6401815c2f69923a1d74eb4797409729.atoum.cache' autoloader cache file…
Using '/var/www/src/tests/units/App/Http/Api/Transformers/OpenGate.php' test file…
Using '/var/www/src/tests/units/App/Http/Middleware/Paginate.php' test file…
Using '/var/www/src/tests/units/App/Models/Database/Holiday.php' test file…
Using '/var/www/src/tests/units/App/Models/Database/Parking/Booking.php' test file…
Using '/var/www/src/tests/units/App/Models/Database/Parking/Parking.php' test file…
Using '/var/www/src/tests/units/App/Models/Projections/Profile/CreditRequest.php' test file…
> atoum path: /var/www/src/vendor/atoum/atoum/bin/atoum
> atoum version: 3.1.1
> PHP path: /usr/local/bin/php
> PHP version:
=> PHP 7.1.9 (cli) (built: Sep  8 2017 02:55:21) ( NTS )
=> Copyright (c) 1997-2017 The PHP Group
=> Zend Engine v3.1.0, Copyright (c) 1998-2017 Zend Technologies
=>     with Xdebug v2.5.5, Copyright (c) 2002-2017, by Derick Rethans

Configuration: example

use mageekguy\atoum\reports;
use mageekguy\atoum\reports\coverage;
use mageekguy\atoum\writers\std;
use mageekguy\atoum\report\fields\runner\result\logo;
// path & branch coverage for better view on the coverage
$script->enableBranchAndPathCoverage();
// automatically run test in this directory
$runner->addTestsFromDirectory(__DIR__ . '/tests/units');
// add report extension
$report = $script->addDefaultReport();
$extension = new reports\extension($script);  $extension->addToRunner($runner);
// html report & stdout report
$coverage = new coverage\html();
$coverage->addWriter(new std\out());
$coverage->setOutPutDirectory(__DIR__ . '/tests/reports/unit/');
$runner->addReport($coverage);
// telemetry platform report
$telemetry = new reports\telemetry();
$telemetry->addWriter(new std\out());
$telemetry->readProjectNameFromComposerJson(__DIR__ . '/composer.json');
$telemetry->sendAnonymousProjectName();   $runner->addReport($telemetry);
// for the fun, a nice cli logo
$report->addField(new logo());

Asserters

<?php

namespace A\B {
  class C {
    public function iMReturningABool() {
      return false;
    }
  }
}

namespace A\test\unit\B {
  class C extends \atoum {
    public function testIMReturningABool() {
      $this
        ->given($this->newTestedInstance())
          ->assert('Will return false')
            ->boolean($this->testedInstance->iMReturningABool())
              ->isFalse;
    }
  }
}

Asserters: tree

|-- error
|-- mock
|-- stream
`-- variable
  |-- array
  |    `-- castToArray
  |-- boolean
  |-- class
  |    `-- testedClass
  |-- integer
  |   |-- float
  |   `-- sizeOf
  ...
|-- object
|   |-- dateInterval
|   |-- dateTime
|   |   `-- mysqlDateTime
|   |-- exception
|   `-- iterator
|       `-- generator
|-- resource
`-- string
  |-- castToString
  |-- hash
  |-- output
  `-- utf8String

Asserters: assertion

// () or no ?
$this->boolean(true)->isTrue;
$this->boolean(true)->isTrue();
$this->boolean(true)->isTrue('PHP is going crazy!');
// some sugar
$a = ['foo' => 42, 'bar' => '1337'];
$this
   ->array($a)
      ->integer['foo']->isEqualTo(42)
      ->string['bar']->isEqualTo('1337');

Asserters: Aliases

$a = 42;
$this->integer($a)->isIdenticalTo(42);
$this->integer($a)->{'==='}(42);

Asserters: Aliases, custom

  • from([asserter])->use([assertion])->as([assertion alias])
    • $this->[asserter]->[assertion alias]
  • $this->from(‘string’)->use(‘isEqualTo’)->as(‘is’);
    • $this->string($atoum)->is(‘atoum’);
  • $this->from(‘float’)->use(‘isNearlyEqualTo’)->as(‘~=’);
    • $this->float(1 / 3)->{‘~=‘}(0.3, 0.1);
  • constructor, setUp, test method, test class, bootstrap or config

Exception handling

<?php
// ...
$this
    ->exception(
        function() {
            $this->testedInstance->doOneThing('wrongParameter');
        }
    )->hasMessage('My foo exception message')
    ->isInstanceOf(FooException::class)
;

anonymous function are also used for output

Mock

  • Mock & data set are the most important part of your tests

  • Autoloading

  • Natural language declaration

  • Mock everything

Mock

  • Warning

    • Not empty by default!

    • => Keep the feature in the mock

  • Mock everything

    • interface,

    • class,

    • abstract,

    • function

    • ....

Mock: creation

$mock = new \mock\Foo\Bar($baz, $args);
// same as, but without IDE complains
$mock = $this->newMockInstance(\Foo\Bar::class, null, null, [$baz, $arg]);

$this->mockGenerator->generate(
    '\Vendor\Project\AbstractClass',
    '\MyMock',
    'MockClass');
$mock = new \myMock\MockClass;
// same as
$mock = $this->newMockInstance(
    '\Vendor\Project\AbstractClass',
    '\MyMock',
    'MockClass');

Mock: class generated

<?php
class Bar {
  public function __construct(at $a, oum $b) {/*...*/}
  public function foo($arg) {/*...*/}
}
$mock = new \mock\Bar($a, $b);

// the created mock will look like if we write this
namespace mock {
  class Bar extends \Bar {
    public function __construct(at $a, oum $b) {
      parent::__construct($a, $b);
    }

    public function foo($arg) {
      parent::foo($arg);
    }
  }

Mock: empty behaviour

shuntParent mock

<?php
class Bar {
  public function __construct(at $a, oum $b) {/*...*/}
  public function foo($arg) {/*...*/}
}
$this->mockGenerator->shuntParentClassCalls();
$mock = new \mock\Bar($a, $b);

// the created mock will look like if we write this
namespace mock {
  class Bar extends \Bar {
    public function __construct(at $a, oum $b) {} // no code to parent call

    public function foo($arg) {}
  }
}

Mock: empty method

orphanize mock

<?php
class Bar {
  public function __construct(at $a, oum $b) {/*...*/}
  public function foo($arg) {/*...*/}
}
$this->mockGenerator->orphanize('__construct'); // can be any method we want empty
$mock = new \mock\Bar($a, $b);

// the created mock will look like if we write this
namespace mock {
  class Bar extends \Bar {
    public function __construct() {} // no args and no code
    public function foo($arg) {
      parent::foo($arg);
    }
  } }

Mock: change behaviour

  • $this->calling($mock)->foo = ‘value’; // or a callable

  • $this->calling($mock)->foo[3] = 'value on third call';

  • $this->calling($mock)->foo->throw = new Exception();

  • $this->mock($mock)->call(‘foo’)->once;

    • ->twice

    • ->exactly(3) <=> ->{3} <=> ->thrice

    • ->atLeast(2)

  • ​Read the doc, it will have examples and many more

Mock: Interface

new \mock\Countable();

Mock: Constant

$this->constant->PHP_VERSION_ID = 60606;
$this->constant->PHP_VERSION = '6.6.6';
$this->string(PHP_VERSION)->isEqualTo('6.6.6');

Mock: native function

$this
   ->assert('the file exist')
      ->given($this->newTestedInstance())
      ->if($this->function->file_exists = true)
      ->then
      ->object($this->testedInstance->loadConfigFile())
         ->isTestedInstance()
         ->function('file_exists')->wasCalled()->once();

Mock: injected in test method

If you need a mock inside your test method...

... but if it required args, use a data provider!

<?php

// ...
public function testFoo(\Foo\MyInterface $fooInterfaceMock) {
  $this->mock($fooInterfaceMock);
}

Reporting

  • From configuration file
  • Reports extension is nice
    • fun (santa, nyan)
    • ...
  • /!\ xunit report is the standard one...
  • coverage is using xdebug only (for now)

Reporting: coverage

Without path coverage

With path coverage

Reporting: telemetry

Some key difference with PHPUnit 1/2

  • Testing variable types
  • Mocking system
  • Use closure to test outputs, exceptions, …
  • Multiple execution engine
    • Concurrent run of test cases
    • Fully isolated test cases
  • Speed

Some key difference with PHPUnit 2/2

  • Fluent interface
  • no @depends (injecting result of another test inside the following)
  • forced namespace & classname
  • far less permissive by default
  • syntaxic sugar (array, given, if, then, newTestedInstance, ...)
  • Want a PHPUnit like mock? $this->mockGenerator->allIsInterface();
  • (a lot more assertion)
  • extension PHPUnit bridge

Integration in tools

  • IDE: netbeans, PHPStorm, sublime text, vim, atom, ...
  • Task: Phing, Robo, GrumPHP, ...
  • CI: jenkins, circleCI, ContinuousPHP, travis, gitlab, ...
  • Frameworks: Symfony, Zend 2, ezPublish, ...

Extensions

Extensions

Documentation

  • Try to be an help
  • Use rusty for the validity of the example
  • Should normally be up to date or have an issue in github

Tips & tricks

  • -ncc: remove code coverage
  • --loop: use native loop mode
  • -ns A\B: take only class in namespace A\B
  • --debug: $this->dump($data)

Still a lot more... but no more time ;)

  • Data provider
  • Annotation
  • Test hook
  • Differences between the execution engines
  • ...

Questions?

Thanks

Introducing and discovering of atoum, PHPLimburg meetup

By Jonathan Van Belle

Introducing and discovering of atoum, PHPLimburg meetup

In the PHP world, when you speak about unit testing, everybody will know PHPUnit. But there is some alternative that gets a lot of attention lately: atoum. During this talk, we try to understand some of it's specificities and why you should use it. You will discover the simplicity and the accuracy inside unit testing.

  • 2,015