LaravelでPHPUnitを使ってテストする手法

#はじめに
筆者、テストというものの存在は上司から教わり、書き方も自分で調べながら、さぐりさぐりで書いてみたので、
この記事の書き方や説明が必ず正しいという保証はできない。
自分の備忘録的なものも兼ねての記事なので、参考程度に見て頂きたい。

#テストとは何か
簡単に言えば、
*`このアプリ、本当にイメージした通りに動いてるのか?`*
っていうのを、確認してくれるもの。

どういうことかと言うと、

**ここでの表示はこれだ!**
**ここではこの処理をして、こういう結果が帰ってくるんだ！**

というのを、あらかじめすべての処理について、テストに書いておく。
そうすることで、なにか不具合があっても、

**「ここの表示、君のイメージと違うみたいだよ」**
**「ここの処理、君の思うようには動いてないよ」**

という感じで、テスト君が教えてくれるということだ。

#なぜテストを書くのか
例えば、複数人でなんかの開発をしてて、
`「やったーー!!完成した!!!!」`と喜んでいたとしよう。
その後、誰かが`「あ...、ここ手直ししなきゃ。アップデートじゃ!」`という感じで、どこかに手を加えたとする。
すると、`「あれ...、こっちがなんか変だぞ?」`とか、`「俺の作ったクラスが動かなくなってる」`みたいな、
意図しないエラーがあちこちで発生する事が多い。

要するに、ある箇所のソースコードをちょっと書き換えたせいで、他の関数に影響を与えたりするわけ。
大きいプログラムになればなるほど、そのエラーの原因を突き止めるのは大変だろうし、
**最悪、エラーに気づかないままリリースしちゃう**こともある。

こうならないために、テストを書く。
正直、テストを書こうとすると結構大変だが、プログラムが膨大になれば、エラー解決のほうがよっぽど大変だろうと思う。

#テストの準備
Laravelに標準搭載されている`PHPUnit`を使う。
今回は、`app/Http/Controllers/BookController.php`のテストを書くという前提で話を進める。

```
$ php artisan make:test BookTest
```
このコマンドを実行するだけで、テスト用のファイルが`tests/Feature/`に生成される。
上記のように命名するのが一般的らしい。

#テストを体験してみる
先程生成されたファイルを見てみよう。

```php:test/Feature/BookTest.php
<?php

namespace Tests\Feature;
  
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;

class BookTest extends TestCase
{
  /**
   * A basic test example.
   *
   * @return void
   */
  public function testExample()
  {
    $this->assertTrue(true);
  }
}
```
こんなファイルが出来上がっているはず。
`assertTrue()`は、**引数がtrueかどうか**を判定するメソッドだ。

試しに、このままテストを実行してみよう。
テストは以下のようにして実行する。

```
$ ./vendor/bin/phpunit
PHPUnit 7.5.2 by Sebastian Bergmann and contributors.

....                                                                4 / 4 (100%)

Time: 155 ms, Memory: 14.00MB
OK (4 tests, 9 assertions)
```
こんな感じで出力されれば、テストは成功している。
**テストが成功する**ということは、**イメージ通りにプログラムが動いている**ということだ。
(もちろん、テストコードが完璧に記述されていればだが...)

`test`や`assertions`の前の数字は、
実行されたテストの数と、その中で呼ばれた`assertなんちゃら`メソッドの数だ。
筆者の環境だと、既にいくつかのテストを書いているので、皆さんの結果とは数が違うと思う。

今、先程のメソッドを、

```
$this->assertTrue(false);
```
というふうに書き換えて、再度テストを実行すると、

```
$ ./vendor/bin/phpunit
PHPUnit 7.5.2 by Sebastian Bergmann and contributors.

.F..                                                                4 / 4 (100%)

Time: 148 ms, Memory: 14.00MB

There was 1 failure:

1) Tests\Feature\UserTest::testExample
Failed asserting that false is true.

/Users/shindex/laravel/portal/tests/Feature/UserTest.php:18

FAILURES!
Tests: 4, Assertions: 9, Failures: 1.
```
こんな感じで、もちろんエラーとなる。

#より実践的なテスト
実際に筆者が作ったアプリの一部を切り取って、テストを書いてみよう。

##テスト対象の動作
<img width="1168" alt="スクリーンショット 2019-03-14 20.07.16.png" src="https://qiita-image-store.s3.amazonaws.com/0/349288/7d8aeb68-431c-7420-e728-c4f85b4d946c.png">
このように、本の[ISBN](https://ja.wikipedia.org/wiki/ISBN)を入力して`Registerボタン`を押すと、
<img width="1174" alt="スクリーンショット 2019-03-14 20.08.57.png" src="https://qiita-image-store.s3.amazonaws.com/0/349288/07873025-6885-49fe-03ac-1c2280c01301.png">
ISBNを参考にして、本の`タイトル`、`著者`を表示してくれる。
以下の情報でよろしいですか?と聞かれるので、`confirmボタン`を押せば、DBに本が登録されるという動きだ。

今回テストしたいのは

**1. ISBNだけでタイトルと著者が取得されて、表示できているか?**
**2. ちゃんとDBに登録されているか?**

の2点だ。

##テスト対象のソースコード達
まず、ルートをお見せしておく。

|Method|URI|Name|Action|Middleware|
|:--|:--|:--|:--|:--|
|GET|books/create|books.create|App\Http\Controllers\BookController@create|web, auth|
|POST|books|books.store|App\Http\Controllers\BookController@create|web, auth|
|POST|register_from_web||App\Http\Controllers\BookController@register_from_web|web, auth|

ユーザーが本を登録したいとき、まず`url('books/create')`にアクセスする。

```php:app/Http/Controllers/BookController.php
<?php

namespace App\Http\Controllers;
use App\Book;

class BookController extends Controller
{
  (省略)

  public function create()
  {
    return view('books.create');
  }

  public function store(Request $request)
  {
    $isbn = $request->isbn;
    $request_book = $this->api($isbn);
    return view('books.confirm', ['request_book' => $request_book]);
  }

  public function api($isbn)
  {
    $client = new Client();
    $response = $client->request('GET','https://api.openbd.jp/v1/get?isbn='.$isbn);
    $return = json_decode($response->getBody(), true);
    $return_summary = $return[0]['summary'];
    $return_title = $return_summary['title'];
    $return_author = $return_summary['author'];
    $books = [
      'title' => $return_title,
      'author' => $return_author,
      'isbn' => $isbn
    ];
    return $books;
  }

  public function register_from_web(Request $request)
  {
    $book = new Book;
    $book->title = $request['title'];
    $book->author = $request['author'];
    $book->isbn = $request['isbn'];
    $book->save();
    return redirect('/books');
  }

  (省略)
}
```

```html:resources/views/books/create.blade.php
<div class="container">
  <hi>Register Book</h1>
  <hr>
  <form action="{{ url('/books') }}" method="post">
    @csrf
    @method('POST')
    <div class="form-group">
      <label for="isbn">{{ __('Isbn') }}</label>
      <input id="isbn" type="text" class="form-control" name="isbn" autofocus>
    </div>
    <button type="submit" name="submit" class="btn btn-primary" id='submit'>{{ __('Register') }}</button>
  </form>
</div>
```
```html:resources/views/books/confirm.blade.php
<div class="container">
  <h2>Are you happy with the following?</h2>
  <hr>
  <form action="{{ url('register_from_web') }}" method="post">   
    @csrf
    @method('POST')
    
    {{-- 本の内容 --}}
    <div class="form-group">
      <label for="title">{{ __('Title') }}</label>
      <input id="title" type="text" class="form-control" name="title" value="{{ $request_book['title'] }}" required>
    </div>
 
    <div class="form-group">
      <label for="author">{{ __('Author') }}</label>
      <input id="author" type="text" class="form-control" name="author" value="{{ $request_book['author'] }}" required>
    </div>

    <div class="form-group">
      <label for="isbn">{{ __('Isbn') }}</label>
      <input id="isbn" type="text" class="form-control" name="isbn" value="{{ $request_book['isbn'] }}" required>
    </div>

    {{-- 登録 --}}
    <div class="comfirm">
      <button type="submit" name="submit" class="btn btn-primary" id="submit">
        {{ __('Confirm') }}
      </button>
    </div>
  </form>
</div>
```

`create.blade.php`が先程の1枚目、`confirm.blade.php`が2枚目の画像に当たる。
[openBD](https://openbd.jp/)を利用することで、ISBNで`タイトル`と`著者`を取得できる。

##実際にテストコードを書いてみる
実際には`assertTrue()`なんか使わない。
もっと実践的な`assertなんちゃら`メソッドを使ってみよう。

```php:
<?php

namespace Tests\Feature;

use App\Book;
use App\Http\Controllers\BookController;
use Tests\TestCase;
use Illuminate\Support\Facades\Auth;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;

class BookTest extends TestCase
{
  public function test_book_controller()
  {
    Auth::loginUsingId(2);
    $this->store();
  }
  
  private function store()
  {
    $BookController = new BookController;
    $books = $BookController->api('9784197037292');

    $this->post('/books', $books);
         ->assertSee('Are you happy with the following?')
         ->assertSee('Title')
         ->assertSee('Author')
         ->assertSee('Isbn')
         ->assertSee('Confirm')
         ->assertSee('火垂るの墓')
         ->assertSee('野坂昭如／著 高畑勲／著')
         ->assertSee('9784197037292');

    $this->register_from_web($books);
  }

  private function register_from_web($book)
  {
    $this->post('/register_from_web', $book);

    $this->assertDatabaseHas('books', [
      'title' => '火垂るの墓',
      'author' => '野坂昭如／著 高畑勲／著',
      'isbn' => '9784197037292'
    ]);

    Book::where('title', '火垂るの墓')->delete();
  }
```

###テストの書き方と注意点
まず注意したいのが、**関数名**である。
~~テストを実行した際には、**関数名がtestから始まるメソッド**しか呼ばれない。
これは結構なトラップなので、心に留めておくこと。~~

### **追記**
余裕で間違ってます。コメント欄でご指摘いただきました。
こんな感じで、@testというコメントをつけることで、テストとして認識されます。

```php5
/**
 * @test
 */
public function initialBalanceShouldBe0()
{
    $this->assertSame(0, $this->ba->getBalance());
}
```

今回`url('/books/なんちゃら')`は、認証したユーザーしかアクセスできないので、
`id = 2`をテスト用のユーザーとして用意し、そこにログインさせている。

`api()`に「火垂るの墓」のISBNを渡した上で、
タイトルを著者がちゃんと表示されているかを、`assertSee()`でテストしている。**(テストしたいことの1個目)**

その後、実際にDBに登録する`register_from_web()`を呼び、
ちゃんとDBに登録できたのかを、`assertDatabaseHas()`でテストしている。**(テストしたいことの2個目)**

最後に、DBの保存したデータをdeleteして、テストするたびにレコードが追加されることのないようにしている。

今回利用した、テストに関連するメソッドの詳細は、以下の表の通りだ。

|メソッド名|動作|
|:--|:--|
|get('URL')|URLにgetする|
|post('URL', POSTしたいデータ)|指定したデータをURLにpostする|
|assertOk()|レスポンスが200OKかどうか|
|assertSee($value)|指定した文字列がレスポンスに存在するか|
|assertDatabaseHas('テーブル名', 配列)|DBに指定したデータが存在するか|

assertDataHas()の第二引数は、今回だと

```
[
  'title' => '火垂るの墓',
  'author' => '野坂昭如／著 高畑勲／著',
  'isbn' => '9784197037292'
]
```
という感じになっている。

もちろんだが、`assertOk()`や`assertSee()`は、レスポンスが来ていないと使えない。
これらのメソッド以外にも、たくさんのメソッドがある。
利用可能なアサートは[公式](https://readouble.com/laravel/5.7/ja/http-tests.html#assert-see)を参考にしてほしい。
でも正直、今回の事例だと使えるメソッドはこれくらいしかない。

#おわりに
頑張って全てのメソッドに対してテストを書こう...