92
79

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-03-15

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

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

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

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

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

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

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

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

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

こうならないために、テストを書く。
正直、テストを書こうとすると結構大変だが、プログラムが膨大になれば、エラー解決のほうがよっぽど大変だろうと思う。

#テストの準備
Laravelに標準搭載されているPHPUnitを使う。
今回は、app/Http/Controllers/BookController.phpのテストを書くという前提で話を進める。

$ php artisan make:test BookTest

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

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

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)

こんな感じで出力されれば、テストは成功している。
テストが成功するということは、イメージ通りにプログラムが動いているということだ。
(もちろん、テストコードが完璧に記述されていればだが...)

testassertionsの前の数字は、
実行されたテストの数と、その中で呼ばれた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.

こんな感じで、もちろんエラーとなる。

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

##テスト対象の動作
スクリーンショット 2019-03-14 20.07.16.png
このように、本のISBNを入力してRegisterボタンを押すと、
スクリーンショット 2019-03-14 20.08.57.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')にアクセスする。

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');
  }

  (省略)
}
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>
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を利用することで、ISBNでタイトル著者を取得できる。

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

<?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というコメントをつけることで、テストとして認識されます。

/**
 * @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()は、レスポンスが来ていないと使えない。
これらのメソッド以外にも、たくさんのメソッドがある。
利用可能なアサートは公式を参考にしてほしい。
でも正直、今回の事例だと使えるメソッドはこれくらいしかない。

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

92
79
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
92
79

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?