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
- Extension mechanism
- Installation
- autoloaded with composer
- using configuration file
- http://extensions.atoum.org/
Extensions
- visibility
- json-schema
- ruler
- ide-helper
- autoloop
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,698