Unit Testing

You might have noticed some subtle but nonetheless important bugs in the framework we built in the previous chapter. When creating a framework, you must be sure that it behaves as advertised. If not, all the applications based on it will exhibit the same bugs. The good news is that whenever you fix a bug, you are fixing a bunch of applications too.

前の章で構築したフレームワークに、微妙ではあるが重要なバグがいくつかあることに気付いたかもしれません。フレームワークを作成するときは、宣伝どおりに動作することを確認する必要があります。そうでない場合、それをベースにしたすべてのアプリケーションで同じバグが発生します。良いニュースは、バグを修正するたびに、多数のアプリケーションも修正していることです。

Today's mission is to write unit tests for the framework we have created by using PHPUnit. At first, install PHPUnit as a development dependency:

今日のミッションは、PHPUnit を使用して作成したフレームワークの単体テストを作成することです。最初に、PHPUnit を開発用の依存関係としてインストールします。
1
$ composer require --dev phpunit/phpunit

Then, create a PHPUnit configuration file in example.com/phpunit.xml.dist:

次に、example.com/phpunit.xml.dist に PHPUnit 構成ファイルを作成します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
    backupGlobals="false"
    colors="true"
    bootstrap="vendor/autoload.php"
>
    <coverage processUncoveredFiles="true">
        <include>
            <directory suffix=".php">./src</directory>
        </include>
    </coverage>

    <testsuites>
        <testsuite name="Test Suite">
            <directory>./tests</directory>
        </testsuite>
    </testsuites>
</phpunit>

This configuration defines sensible defaults for most PHPUnit settings; more interesting, the autoloader is used to bootstrap the tests, and tests will be stored under the example.com/tests/ directory.

この構成は、ほとんどの PHPUnit 設定に対して適切なデフォルトを定義します。さらに興味深いことに、オートローダーはテストのブートストラップに使用され、テストは example.com/tests/ ディレクトリの下に保存されます。

Now, let's write a test for "not found" resources. To avoid the creation of all dependencies when writing tests and to really just unit-test what we want, we are going to use test doubles. Test doubles are easier to create when we rely on interfaces instead of concrete classes. Fortunately, Symfony provides such interfaces for core objects like the URL matcher and the controller resolver. Modify the framework to make use of them:

それでは、「見つからない」リソースのテストを書きましょう。テストを書くときにすべての依存関係が作成されるのを避け、実際に必要なものだけを単体テストするために、テスト ダブルを使用します。テストダブルは、具体的なクラスではなくインターフェイス上にある場合に簡単に作成できます。幸いなことに、Symfony は、URL マッチャーやコントローラー リゾルバーなどのコア オブジェクト用のインターフェイスを提供しています。それらを利用するためにフレームワークを変更します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// example.com/src/Simplex/Framework.php
namespace Simplex;

// ...

use Calendar\Controller\LeapYearController;
use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface;
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;

class Framework
{
    protected $matcher;
    protected $controllerResolver;
    protected $argumentResolver;

    public function __construct(UrlMatcherInterface $matcher, ControllerResolverInterface $resolver, ArgumentResolverInterface $argumentResolver)
    {
        $this->matcher = $matcher;
        $this->controllerResolver = $resolver;
        $this->argumentResolver = $argumentResolver;
    }

    // ...
}

We are now ready to write our first test:

これで最初のテストを書く準備が整いました:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// example.com/tests/Simplex/Tests/FrameworkTest.php
namespace Simplex\Tests;

use PHPUnit\Framework\TestCase;
use Simplex\Framework;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface;
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
use Symfony\Component\Routing;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;

class FrameworkTest extends TestCase
{
    public function testNotFoundHandling()
    {
        $framework = $this->getFrameworkForException(new ResourceNotFoundException());

        $response = $framework->handle(new Request());

        $this->assertEquals(404, $response->getStatusCode());
    }

    private function getFrameworkForException($exception)
    {
        $matcher = $this->createMock(Routing\Matcher\UrlMatcherInterface::class);
        // use getMock() on PHPUnit 5.3 or below
        // $matcher = $this->getMock(Routing\Matcher\UrlMatcherInterface::class);

        $matcher
            ->expects($this->once())
            ->method('match')
            ->will($this->throwException($exception))
        ;
        $matcher
            ->expects($this->once())
            ->method('getContext')
            ->will($this->returnValue($this->createMock(Routing\RequestContext::class)))
        ;
        $controllerResolver = $this->createMock(ControllerResolverInterface::class);
        $argumentResolver = $this->createMock(ArgumentResolverInterface::class);

        return new Framework($matcher, $controllerResolver, $argumentResolver);
    }
}

This test simulates a request that does not match any route. As such, the match() method returns a ResourceNotFoundException exception and we are testing that our framework converts this exception to a 404 response.

このテストは、どのルートとも一致しないリクエストをシミュレートします。そのため、match() メソッドは ResourceNotFoundException 例外を返し、フレームワークがこの例外を 404 応答に変換することをテストします。

Execute this test by running phpunit in the example.com directory:

example.com ディレクトリで phpunit を実行して、このテストを実行します。
1
$ ./vendor/bin/phpunit

Note

ノート

If you don't understand what the hell is going on in the code, read the PHPUnit documentation on test doubles.

コードで一体何が起こっているのか理解できない場合は、テストダブルに関する PHPUnit のドキュメントを読んでください。

After the test ran, you should see a green bar. If not, you have a bug either in the test or in the framework code!

テストの実行後、緑色のバーが表示されます。そうでない場合は、テストまたはフレームワーク コードにバグがあります。

Adding a unit test for any exception thrown in a controller:

コントローラーでスローされた例外の単体テストを追加します。
1
2
3
4
5
6
7
8
public function testErrorHandling()
{
    $framework = $this->getFrameworkForException(new \RuntimeException());

    $response = $framework->handle(new Request());

    $this->assertEquals(500, $response->getStatusCode());
}

Last, but not the least, let's write a test for when we actually have a proper Response:

最後になりましたが、実際に適切なResponseがある場合のテストを書きましょう:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver;
use Symfony\Component\HttpKernel\Controller\ControllerResolver;
// ...

public function testControllerResponse()
{
    $matcher = $this->createMock(Routing\Matcher\UrlMatcherInterface::class);
    // use getMock() on PHPUnit 5.3 or below
    // $matcher = $this->getMock(Routing\Matcher\UrlMatcherInterface::class);

    $matcher
        ->expects($this->once())
        ->method('match')
        ->will($this->returnValue([
            '_route' => 'is_leap_year/{year}',
            'year' => '2000',
            '_controller' => [new LeapYearController(), 'index'],
        ]))
    ;
    $matcher
        ->expects($this->once())
        ->method('getContext')
        ->will($this->returnValue($this->createMock(Routing\RequestContext::class)))
    ;
    $controllerResolver = new ControllerResolver();
    $argumentResolver = new ArgumentResolver();

    $framework = new Framework($matcher, $controllerResolver, $argumentResolver);

    $response = $framework->handle(new Request());

    $this->assertEquals(200, $response->getStatusCode());
    $this->assertStringContainsString('Yep, this is a leap year!', $response->getContent());
}

In this test, we simulate a route that matches and returns a simple controller. We check that the response status is 200 and that its content is the one we have set in the controller.

このテストでは、一致して simplecontroller を返すルートをシミュレートします。応答ステータスが 200 であることと、その内容がコントローラーで設定したものであることを確認します。

To check that we have covered all possible use cases, run the PHPUnit test coverage feature (you need to enable XDebug first):

考えられるすべてのユース ケースをカバーしていることを確認するには、PHPUnit の testcoverage 機能を実行します (最初に XDebug を有効にする必要があります)。
1
$ ./vendor/bin/phpunit --coverage-html=cov/

Open example.com/cov/src/Simplex/Framework.php.html in a browser and check that all the lines for the Framework class are green (it means that they have been visited when the tests were executed).

ブラウザーで example.com/cov/src/Simplex/Framework.php.html を開き、Framework クラスのすべての行が緑色であることを確認します (テストが実行されたときにアクセスされたことを意味します)。

Alternatively you can output the result directly to the console:

または、結果をコンソールに直接出力することもできます。
1
$ ./vendor/bin/phpunit --coverage-text

Thanks to the clean object-oriented code that we have written so far, we have been able to write unit-tests to cover all possible use cases of our framework; test doubles ensured that we were actually testing our code and not Symfony code.

これまでに作成したクリーンなオブジェクト指向コードのおかげで、フレームワークの考えられるすべてのユース ケースをカバーする単体テストを作成できました。 test doubles により、Symfony のコードではなく、実際にコードをテストしていることを確認できました。

Now that we are confident (again) about the code we have written, we can safely think about the next batch of features we want to add to our framework.

作成したコードについて (再び) 確信が持てたので、フレームワークに追加する機能の次のバッチについて安全に考えることができます。