atoum, introduction and discovering

PHP Leuven meetup

2017-10-05

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

 

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
        ->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',
    'AClass');
$mock = new \myMock\AClass;
// same as
$mock = $this->newMockInstance(
    '\Vendor\Project\AbstractClass',
    '\MyMock',
    'AClass');

Mock: class generated

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

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

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

Mock: Interface

new \mock\Countable();

Mock: empty behaviour

shuntParent mock

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

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

    public function foo($arg) {}
  }
}

Mock: empty method

orphanize mock

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

// the created mock will look like if we write this
namespace mock {
  class klass extends \klass {
    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)->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

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

Introducing and discovering of atoum

By Jonathan Van Belle

Introducing and discovering of atoum

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. And even if you already know atoum, you will most likely discover something new about the latest upcoming functionality.

  • 1,720