[Drupal]PHPUnitで基本のカーネルテスト


## カーネルテストとは
DrupalのPHPUnitのテストには主にユニットテスト、カーネルテスト、ファンクショナルテストというものがあります。

![スクリーンショット 2022-04-19 8.12.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253501/0427df25-8c3a-80db-5f27-e46e9772f380.png)

ユニットテストはDrupalのブートストラップ無し状態で行うテストです。一番高速ですがDrupalのサービスクラスなどを呼び出す際にいわゆる「モック」を作らなくてはいけないというデメリットがあります。またユニットテストでDBに接続するのは（頑張ればできるけど）非推奨とされています。

ファンクショナルテストはいわゆるブラウザテストで、新しくインストールされたDrupalに対して仮想ブラウザからリクエストを送る形でテストを行います。DrupalのコアとDBが使えたりユーザー目線のテストができるという反面実行速度が遅いというデメリットがあります。

カーネルテストはユニットテストとファンクショナルテストの中間に位置するもので、Drupalカーネルのブートストラップがされている状態なのでDrupalの各種サービスやDBは使えるけど、仮想ブラウザからのアクセスは行えないというものです。ユニットテストのようにサービスを使うたびにモックを書かなくてもよく、ファンクショナルテストより速いというメリットがあります。リクエストを行わないようなバックエンドの処理のテストに最適なのはもちろんですが、[こちらのドキュメント](https://www.drupal.org/docs/automated-testing/phpunit-in-drupal/making-http-requests-programmatically-in-kernel-tests)を読む限りHTTPリクエストも作れるので、どうしてもブラウザからアクセスしないとダメっていう場合（UIのテスト等？）以外のファンクショナルテストはカーネルテストに落とし込むことができるのではないかなと思います。

ここらへんの話はこちらの記事が詳しく参考になりました :pray: 

https://blog.irologue.com/drupal/20211206/


## 基本のカーネルテスト

今回はこんなサービスとテストを作成してみます。
- 「[現在のユーザーのユーザー名]さん、Hello World!」という文字列を取得するだけのパブリックメソッドを持つサービスを作成する
- 上記のメソッドがちゃんと「[現在のユーザーのユーザー名]さん、Hello World!」という文字列を返すかをテストする

（カーネルテストの特徴として「DBや構成にアクセスできる」というものがあるので、現在のユーザーのユーザー名にアクセスするようにしてみました）

### テスト環境の用意
DrupalでPHPUnitのテストをする場合はまず以下のように`phpunit.xml`を編集する必要があります。
- `app/core/phpunit.xml.dist`をコピーして`app/core/phpunit.xml`にリネーム
- `phpunit.xml`を以下のように編集
    - `<env name="SIMPLETEST_BASE_URL" value=""/>`の`value`にサイトのベースURLを入力（例: `http://localhost:8080`）

    - `<env name="SIMPLETEST_DB" value=""/>`の`value`にデータベースURLを入力（例: `mysql://username@localhost/my_database`）
- この時点で`core`ディレクトリに移動し`../../vendor/bin/phpunit [任意のディレクトリ]`を入力し、そのでディレクトリ下のテストが実行されればOK（特にコアやコントリビュートモジュールの`/test/src/Unit`（ユニットテストのディレクトリ）が早く実行できるのでオススメ）
```
../../vendor/bin/phpunit modules/datetime/tests/src/Unit

PHPUnit 7.5.20 by Sebastian Bergmann and contributors.

Testing Drupal\Tests\datetime\Unit\Plugin\migrate\field\DateFieldTest
......                                                              6 / 6 (100%)

Time: 191 ms, Memory: 6.00 MB
```

### サービスの作成
以下のようにサービスファイルと`services.yml`を作成してカスタムモジュールに配置します。

#### my_module/src/Services/MyModuleService.php 
```php
<?php

namespace Drupal\my_module\Services;

use Drupal\Core\Session\AccountInterface;

/**
 * MyModuleのサービス.
 */
class MyModuleService {

  /**
   * コンストラクタ.
   *
   * @param \Drupal\Core\Session\AccountInterface $current_user
   *   現在のユーザー.
   */
  public function __construct(AccountInterface $current_user) {
    $this->currentUser = $current_user;
  }

  /**
   * 「現在のユーザーのユーザー名]さん、Hello World!」という文字列を返却する.
   *
   * @return string
   *   「現在のユーザーのユーザー名]さん、Hello World!」という文字列.
   */
  public function getHelloWorld() {
    $name = $this->currentUser->getAccountName();
    return $name . 'さん、Hello World!';
  }

}


```
### my_module/my_module.services.yml
サービス名は`my_module_service`としました。サービスクラスのコンストラクタで使えるように`current_user`サービスを注入しています。
```yaml
services:
  my_module_service:
    class: Drupal\my_module\Services\MyModuleService
    arguments: ['@current_user']
```

この時点で`drush php`でサービスが使えるか軽く確認します。`you have requested a non-existent service`等のエラー等が出る場合はymlやサービスクラスの内容が合ってるかもう一度確認してみてください。
```php
>>> \Drupal::service('my_module_service')->getHelloWorld();
=> "さん、Hello World!"
```
（`drush php`のときは匿名ユーザーなので名前は空）


### テストの作成

上記のサービスに対するテストを作成します。

####  my_module/tests/src/Kernel/MyModuleTest.php
```php
<?php

namespace Drupal\my_module\tests\Kernel;

use Drupal\KernelTests\KernelTestBase;

/**
 * MyModuleサービスのテスト.
 */
class MyModuleTest extends KernelTestBase {

  use \Drupal\Tests\user\Traits\UserCreationTrait;

  /**
   * {@inheritdoc}
   */
  protected static $modules = [
    'user',
    'my_module',
  ];

  /**
   * {@inheritdoc}
   */
  protected function setUp() {
    parent::setUp();

    $this->currentUser = $this->setUpCurrentUser();

    $this->myService = \Drupal::service('my_module_service');
  }

  /**
   * HelloWorldを取得できるかをテストする.
   */
  public function testHelloWorld() {
    $hello_world = $this->myService->getHelloWorld();
    $name = $this->currentUser->getAccountName();
    $this->assertEqual($hello_world, $name . 'さん、Hello World!');
  }

}
```

ポイントは以下です。
- ファイル名は`XXTest.php`にする必要があります。（ディレクトリは適当でもいいっぽい?）
- `\Drupal::currentUser()`で現在のユーザーを取得することもできますが、匿名（ユーザー名無し）になってしまうので、[こちらのドキュメント](https://www.drupal.org/node/3036709)の方法で認証済みユーザー（ユーザー名有り）を現在のユーザーとして取得しました。
- `$modules`に`user`を入れないと`Call to undefined function Drupal\Tests\user\Traits\user_password()`というエラーになるので入れました。
- テストケースは`testXX`という名前のパブリックメソッドにする必要があります。今回は`testHelloWorld()`としました。
- `assertEqual`などカーネルテストで使用できるメソッドは[こちらのドキュメント](https://api.drupal.org/api/drupal/core%21tests%21Drupal%21KernelTests%21KernelTestBase.php/class/KernelTestBase/9.1.x)をご覧ください。

## テストの実行
テストコードが出来たら、さきほどのように`../../vendor/bin/phpunit ../modules/custom/my_module` を実行し、以下のように表示されれば成功です。

```
Testing ../modules/custom/my_module
.                                                                   1 / 1 (100%)

Time: 709 ms, Memory: 6.00 MB
```

以下のようにテスト中にアウトプットを行いたい場合は、
```php
  public function testHelloWorld() {
    $hello_world = $this->myService->getHelloWorld();
    $name = $this->currentUser->getAccountName();
    var_dump($name);
    $this->assertEqual($hello_world, $name . 'さん、Hello World!');
  }
```
`phpunit.xml`の`beStrictAboutOutputDuringTests`を`false`にし、コマンドに`--debug`オプションを付けると以下のようにアウトプットを取得できます。（`beStrictAboutOutputDuringTests`がtrueのままでも出力できますが、エラーメッセージが表示され見づらくなります）

```
Testing ../modules/custom/my_module
Test 'Drupal\my_module\test\Kernel\MyModuleTest::testHelloWorld' started
Test 'Drupal\my_module\test\Kernel\MyModuleTest::testHelloWorld' ended
rbfw3FyF <- $nameの値

Time: 676 ms, Memory: 6.00 MB
```

というわけで基本のカーネルテストの作り方でした。

## 参考
- [Drupal9 最小の力で用意するテストコード戦略](https://blog.irologue.com/drupal/20211206/)
- [Making HTTP requests programmatically in Kernel tests](https://www.drupal.org/docs/automated-testing/phpunit-in-drupal/making-http-requests-programmatically-in-kernel-tests)
- [Do you need a Functional test for that?](https://mglaman.dev/blog/do-you-need-functional-test)
- [KernelTestBase](https://www.drupal.org/docs/automated-testing/phpunit-in-drupal/kerneltestbase)
- [Writing Automated Tests in Drupal 8, Part 4: Kernel tests](https://deninet.com/blog/2019/02/10/writing-automated-tests-drupal-8-part-4-kernel-tests)
- [Instantiating a Database Connection Object](https://www.drupal.org/docs/drupal-apis/database-api/instantiating-a-database-connection-object)