CakePHP3 でメール送信をテストする

## やりたいこと
テストコードで `Cake\Mailer\Email::send()` で送信したメールを確認したいのです。

## 環境
* CakePHP version 3.3.13
* PHPUnit version 5.7.21

## ざっくりした仕組み
CakePHPでは [`Cake\Mailer\Transport\DebugTransport`](https://api.cakephp.org/3.3/class-Cake.Mailer.Transport.DebugTransport.html) があり、e-mailのトランスポートにこれを指定します。そうすると、`Cake\Mailer\Email::send()` の戻り値で送信内容を得られます。

しかし、`send()` の戻り値を使ってテストコードを書くのは違うので、`send()`の引数に渡された `Cake\Mailer\Email` を捕まえてテストできる状態にします。

## コード

### TestableTransport.php
`DebugTransport` を継承して、`send()` の引数を staticなプロパティに貯めておきます。貯めたものは `sendMailLst()` で取得できます。

```php:tests/Mailer/Transport/TestableTransport.php
namespace App\Test\Mailer\Transport;

use Cake\Mailer\Transport\DebugTransport;
use Cake\Mailer\Email;

/**
 * Class TestableTransport
 * @package Mailer\Transport
 */
class TestableTransport extends DebugTransport
{
    /** @var array 送信したメール */
    private static $mailList = [];

    /**
     * 送信済みメールをリセットします。
     */
    public static function reset()
    {
        static::$mailList = [];
    }

    /**
     *
     * @return array 送信したメールのリスト
     */
    public static function sendMailLst()
    {
        return static::$mailList;
    }

    /**
     * @inheritdoc
     */
    public function send(Email $email)
    {
        static::$mailList[] = clone $email;
        return parent::send($email);
    }

}
```

### phpunit.xml.dist
テストクラスにトラスポートを指定するコードを書くことができますが、環境変数 `CAKEPHP_ENV` にPHPUnit用のコンフィグレーションを指定します。

```xml:phpunit.xml.dist
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
        colors="true"
        processIsolation="false"
        stopOnFailure="false"
        syntaxCheck="false"
        bootstrap="./tests/bootstrap.php"
>
    <php>
        <ini name="memory_limit" value="-1"/>
        <ini name="apc.enable_cli" value="1"/>

        <env name="CAKEPHP_ENV" value="phpunit"/>

    </php>

    <!-- Add any additional test suites you want to run here -->
    <testsuites>
        <testsuite name="App Test Suite">
            <directory>./tests/TestCase</directory>
        </testsuite>
        <!-- Add plugin test suites here. -->
    </testsuites>

    <!-- Setup a listener for fixtures -->
    <listeners>
        <listener
                class="\Cake\TestSuite\Fixture\FixtureInjector"
                file="./vendor/cakephp/cakephp/src/TestSuite/Fixture/FixtureInjector.php">
            <arguments>
                <object class="\Cake\TestSuite\Fixture\FixtureManager" />
            </arguments>
        </listener>
    </listeners>

    <!-- Ignore vendor tests in code coverage reports -->
    <filter>
        <whitelist>
            <directory suffix=".php">./src/</directory>
            <directory suffix=".php">./plugins/*/src/</directory>
        </whitelist>
    </filter>
</phpunit>
```

### config/environments/phpunit.php
PHPUnit用のコンフィグレーションで e-mailのトランスポートに `TestableTransport` を指定します

```php:config/environments/phpunit.php
    'EmailTransport' => [
        'default' => [
            'className' => 'App\Test\Mailer\Transport\TestableTransport',
        ],
    ],

```
### コントローラー
```php
/**
 * メール送信を行うアクション
 */
public function action()
{
// 〜 省略 〜
    $mail = new Email();

    $mail->from('form@example.co.jp');
    $mail->to('to@example.co.jp');
    $mail->subject('mail subject');
    $mail->send('mail message');
// 〜 省略 〜
}
```

### テストコード
`Cake\TestSuite\IntegrationTestCase` の子クラスでテスト対象のアクションを実行します。

```php
class XXControllerTest extends IntegrationTestCase
{ 
   public function setUp()
    {
        parent::setUp();
        // 送信したメールをリセットする
        TestableTransport::reset();
    }

    public function testMail()
    {
        // メール送信を行うアクションを実行する
        $this->post('/path/to/action', $params);

        // 送信したメールを確認する
        $mails = TestableTransport::sendMailLst();
        $this->assertEquals( 1 , count($mails), '送信したメールが1件だけ存在する');

        /** @var Cake\Mailer\Email $mail */
        $mail = $mails[0];
        $this->assertEquals( ['form@example.co.jp' =>'form@example.co.jp'] , $mail->from(), '差出人');
        $this->assertEquals( ['to@example.co.jp' =>'to@example.co.jp'] , $mail->to(), '送信先');
        $this->assertEquals( 'mail subject' , $mail->subject(), '件名');
        $this->assertEquals( 'mail message', $mail->message(Email::MESSAGE_TEXT), 'メッセージ');
    }
}

```