PHPLimburg meetup
2018-11-21
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
<?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;
}}}
=> Storytelling style!
test is pleasant, readable, understandable
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
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());
<?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;
}
}
}
|-- 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
// () 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');
$a = 42;
$this->integer($a)->isIdenticalTo(42);
$this->integer($a)->{'==='}(42);
<?php
// ...
$this
->exception(
function() {
$this->testedInstance->doOneThing('wrongParameter');
}
)->hasMessage('My foo exception message')
->isInstanceOf(FooException::class)
;
anonymous function are also used for output
Mock & data set are the most important part of your tests
Autoloading
Natural language declaration
Mock everything
Warning
Not empty by default!
=> Keep the feature in the mock
Mock everything
interface,
class,
abstract,
function
....
$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');
<?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);
}
}
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) {}
}
}
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);
}
} }
$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
new \mock\Countable();
$this->constant->PHP_VERSION_ID = 60606;
$this->constant->PHP_VERSION = '6.6.6';
$this->string(PHP_VERSION)->isEqualTo('6.6.6');
$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();
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);
}
Without path coverage
With path coverage