Test
Writing appropriate tests will help you write better software.
Clean BEAR.Sunday application where all dependencies are injected and cross-cutting concerns are offered at AOP is test friendly. You can write high coverage tests without framework specific tightly coupled base classes or helper methods.
Run test
Run vendor/bin/phpunit
or composer test
. Other commands are as follows.
composer coverge // test coverage
composer cs // very coding standard
composer cs-fix // fix coding standard
Create resource test case
Everything is a resource - BEAR.Sunday application can be tested with resoure access.
This is a test that tests that 201 (Created)
will be returned by POSTing ['title' => 'test']
to URI page://self/todo
of Myvendor\MyProject
application in html-app
context.
<?php
class TodoTest extends TestCase
{
/**
* @var \BEAR\Resource\ResourceInterface
*/
private $resource;
protected function setUp()
{
$this->resource = (new AppInjector('Myvendor\MyProject', 'html-app'))->getInstance(ResourceInterface::class);
}
public function testOnPost()
{
$page = $this->resource->post->uri('page://self/todo')(['title' => 'test']);
/* @var $page ResourceObject */
$this->assertSame(StatusCode::CREATED, $page->code);
}
}
- Please refer to App/TodoTest for CRUD test on App resource.
- For Page Resource testing, please refer to Page/Index.
Application Injector
An application-injector (AppInjector) can generate instances of all classes used in an application with a specific context, and can directly test resource objects and their dependencies.
$injector = new AppInjector('MyVendor\MyProject', 'test-app'));
// resource client
$resource = $injector->getInstance(ResourceInterface::class);
$index = $resource->uri('page://self/index')();
/* @var $index Index */
$this->assertSame(StatusCode::OK, $page->code);
$todos = $page->body['todos'];
// Generate resource class directly
$user = $resource->newInstance('app://self/user');
// or
$user = $injector->getInstance(User::class);
$name = $index->onGet(1)->body['name']; // BEAR
// Verify form validation
$form = $injector->getInstance(TodoForm::class);
$submit = ['name' => 'BEAR'];
$isValid = $this->form->apply($submit); // true
Test Double
Test Double is a generic term for any case where you replace a production object for testing purposes.
In BEAR.Sunday where all dependencies are injected, it is easy to realize a test double with DI, but the test double framework Ray.TestDouble Will make it even more useful “Spy” will be available as well.
composer install
$ composer require ray/test-double 1.x-dev --dev
TestModule
and create modules and install modules.
use BEAR\Package\AbstractAppModule;
use Ray\TestDouble\TestDoubleModule;
class AppModule extends AbstractAppModule
{
protected function configure()
{
// ...
$this->install(new TestDoubleModule);
}
}
We will annotate @Fakeable
for the subject of the test double.
use Ray\TestDouble\Annotation\Fakeable;
/**
* @Fakeable
*/
class Foo
{
public function getDate() {
return date("Ymd");
}
}
Save the test double class to tests/fake-src
folder with Fake
prefix attached. We only implement classes that will extend and replace the original class.
class FakeFoo extend Foo
{
public function getDate() {
return '20170801'; // A stub that simply returns a value
}
}
Add autoload-dev
to composer.json
so that autoload works.
"autoload-dev": {
"psr-4": {
"MyVendor\\MyProject\\": "tests/fake-src"
}
},
FakeFoo
will be called instead of Foo
in test-*
context.
$resource = (new AppInjector('MyVendor\MyProject', 'test-app'))->getInstance(ResourceInterface::class);
Spy
Annotate the class to spy ** with @Spy
to record the input / output target class.
<?php
use Ray\TestDouble\Annotation\Spy;
/**
* @Spy
*/
class Calc
{
public function add($a, $b)
{
return $a + $b;
}
}
Spy::GetLogs($className, $methodName)
will returns the SpyLog
value object containing method arguments and execution results. You can test the input / output and the number of calls of classes.
public function testSpy()
{
$injector = (new AppInjector('MyVendor\MyProject', 'test-app'))->getInstance(InjectorInterface::class);
$calc = $injector->getInstance(Calc::class);
$result = $calc->add(1, 2); // 3
// get spy logs
$spy = $injector->getInstance(Spy::class);
$logs = $spy->getLogs(Calc::class, 'add');
$this->assertSame(1, count($logs)); // call time
/* @var $log SpyLog */
$log = $logs[0]; // first call log
// check arugments and result of method call of `@Spy` annotated method or class.
$this->assertSame([1, 2], $log->arguments);
$this->assertSame(3, $log->result);
}
You can also spy the Fake class and inspect calls to test double.
/**
* @Spy
*/
class FakeUserRole extend UserRole
{
public function getRoleById(string $id) : string
{
// ...条件
return $role
}
}
Bindings using anonymous classes
You can temporarily bind dependencies with PHP’s anonymous class.
public function testAnonymousClassBinding()
$injector = new AppInjector('FakeVendor\HelloWorld', 'hal-app');
$module = new class extends AbstractModule {
protected function configure()
{
$this->bind(FooInterface::class)->to(Foo::class);
}
};
app');
$index = $injector->getOverrideInstance($module, Index::class);
$name = $index(['id' => 1])->body['name'];
$this->assertSame('BEAR', $name);
}
Binding the stub
You can also create a stub with a mocking tool such as phpunit createMock()
method and bind it with that instance.
public function testStub()
{
$injector = new AppInjector('FakeVendor\HelloWorld', 'hal-app');
$stub = $this->createMock(FooInterface::class);
$stub->method('doSomething')
->willReturn('foo');
$module = new class($stub) extends AbstractModule {
private $stub;
public function __construct(FooInterface $stub)
{
$this->stub = $stub;
}
protected function configure()
{
$this->bind(FooInterface::class)->toInstance($this->mock);
}
};
$index = $injector->getOverrideInstance($module, Index::class);
$name = $index(['id' => 1])->body['name'];
$this->assertSame('BEAR', $name);
}
Best Practice
- Test the interface, not the implementation.
- Prefer fake class. The stub is ok. Mock should be avoid in complicated usage.
Reference
This document needs to be proofread by an native speaker.