Test

Proper testing makes software better with continuity. A clean application of BEAR.Sunday is test friendly, with all dependencies injected and crosscutting interests provided in the AOP.

Run test

Run vendor/bin/phpunit or composer test. Other commands are as follows.

composer test    // phpunit test
composer tests   // test + sa + cs
composer coverge // test coverage
composer pcov    // test coverage (pcov)
composer sa      // static analysis
composer cs      // coding standards check
composer cs-fix  // coding standards fix

Resource test

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

use BEAR\Resource\ResourceInterface;

class TodoTest extends TestCase
{
    private ResourceInterface $resource;
    
    protected function setUp(): void
    {
        $injector = Injector::getInstance('test-html-app');
        $this->resource = $injector->getInstance(ResourceInterface::class);
    }

    public function testOnPost(): void
    {
        $page = $this->resource->post('page://self/todo', ['title' => 'test']);
        $this->assertSame(StatusCode::CREATED, $page->code);
    }
}

Test Double

A Test Double is a substitute that replaces a component on which the software test object depends. Test doubles can have the following patterns

  • Stub (provides “indirect input” to the test target)
  • Mock ( validate “indirect output” from the test target inside a test double)
  • Spy (records “indirect output” from the target to be tested)
  • Fake (simpler implementation that works closer to the actual object)
  • Dummy (necessary to generate the test target but no call is made)

Test Double Binding

There are two ways to change the bundling for a test. One is to change the bundling across all tests in the context module, and the other is to temporarily change the bundling only for a specific purpose within one test only.

Context Module

Create a TestModule to make the test context available in bootstrap.

class TestModule extends AbstractModule
{
    public function configure(): void
    {
        $this->bind(DateTimeInterface::class)->toInstance(new DateTimeImmutable('1970-01-01 00:00:00'));
        $this->bind(Auth::class)->to(FakeAuth::class);    
    }
}

Injector with test context.

$injector = Injector::getInstance('test-hal-app', $module);

Temporary binding change

Temporary bundle changes for a single test specify the bundle to override with Injector::getOverrideInstance.

public function testBindFake(): void
{
    $module = new class extends AbstractModule {
        protected function configure(): void
        {
            $this->bind(FooInterface::class)->to(FakeFoo::class);
        }
    }
    $injector = Injector::getOverrideInstance('hal-app', $module);
}

Mock

public function testBindMock(): void
{ 
    $mock = $this->createMock(FooInterface::class);
    // expect that update() will be called once and the parameter will be the string 'something'.
    mock->expects($this->once())
             ->method('update')
             ->with($this->equalTo('something'));
    $module = new class($mock) extends AbstractModule {
        public function __constcuct(
            private FooInterface $foo
        ){}
        protected function configure(): void
        {
            $this->bind(FooInterface::class)->toInstance($this->foo);
        }
    };
    $injector = Injector::getOverrideInstance('hal-app', $module);
}

spy

Installs a SpyModule by specifying the interface or class name of the spy target. 1 After running the SUT containing the spy target, verify the number of calls and the value of the calls in the spy log.

public function testBindSpy(): void
{
    $module = new class extends AbstractModule {
        protected function configure(): void
        {
            $this->install(new SpyModule([FooInterface::class]));
        }
    };
    $injector = Injector::getOverrideInstance('hal-app', $module);
    $resource = $injector->getInstance(ResourceInterface::class);
    // Spy logs of FooInterface objects are logged, whether directly or indirectly.
    $resource->get('/');
    // Spyログの取り出し
    $spyLog = $injector->getInstance(\Ray\TestDouble\LoggerInterface::class);
    // @var array<int, Log> $addLog
    $addLog = $spyLog->getLogs(FooInterface, 'add');   
    $this->assertSame(1, count($addLog), 'Should have received once');
    // Argument validation from SUT
    $this->assertSame([1, 2], $addLog[0]->arguments);
    $this->assertSame(1, $addLog[0]->namedArguments['a']);
}

Dummy

Use Null Binding to bind a null object to an interface.

Hypermedia Test

Resource testing is an input/output test for each endpoint. Hypermedia tests, on the other hand, test the workflow behavior of how the endpoints are connected.

Workflow tests are inherited from HTTP tests and are tested at both the PHP and HTTP levels in a single code. HTTP testing is done with curl and the request/response is logged in a log file.

Best Practice

  • Test the interface, not the implementation.
  • Create a actual fake class rather than using a mock library.
  • Testing is a specification. Ease of reading rather than ease of coding.

Reference

  1. ray/test-double must be installed to use SpyModule.