atoum, introduction and discovering

PHPAntwerp meetup

2017-12-20

First

slides will be available after the talk ;)

Schedule

  • Introduction
  • First test
  • Configuration
  • Asserters
  • Mock
  • Reporting
  • Extra

Who am I?

Grummfy

Jonathan Van Belle

@Grummfy

@Grummfy@mamot.fr

me@grummfy.be

github.com/grummfy

Monizze

 

Open source contributor: atoum, hoa, ...

atoum?

name of an Egyptian god

but we will speak about unit test ;)

fun fact,

atoum was firstly name ogo

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!

Unit test ?

Installation

Phar

local

or

global

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;

$script->enableBranchAndPathCoverage();
$runner->addTestsFromDirectory(__DIR__ . '/tests/units');

$report = $script->addDefaultReport();
$extension = new reports\extension($script);  $extension->addToRunner($runner);

$coverage = new coverage\html();
$coverage->addWriter(new std\out());
$coverage->setOutPutDirectory(__DIR__ . '/tests/reports/unit/');
$runner->addReport($coverage);

$telemetry = new reports\telemetry();
$telemetry->addWriter(new std\out());
$telemetry->readProjectNameFromComposerJson(__DIR__ . '/composer.json');
$telemetry->sendAnonymousProjectName();   $runner->addReport($telemetry);

$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: assertion

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

Mock

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

  • 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: Interface

new \mock\Countable();

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: 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
  • /!\ xunit is the standard one
  • coverage is using xdebug only, for now

Reporting: coverage

Without path coverage

With path coverage

Reporting: telemetry

Extra

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

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, ...
  • 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
  • Standard edition
  • ...

Questions?

Thanks

Feedbacks welcome

https://joind.in/event/php-antwerp---december-meetup/atoum-introduction-and-discovering

Made with Slides.com