#はじめに
筆者、テストというものの存在は上司から教わり、書き方も自分で調べながら、さぐりさぐりで書いてみたので、
この記事の書き方や説明が必ず正しいという保証はできない。
自分の備忘録的なものも兼ねての記事なので、参考程度に見て頂きたい。
#テストとは何か
簡単に言えば、
このアプリ、本当にイメージした通りに動いてるのか?
っていうのを、確認してくれるもの。
どういうことかと言うと、
ここでの表示はこれだ!
ここではこの処理をして、こういう結果が帰ってくるんだ!
というのを、あらかじめすべての処理について、テストに書いておく。
そうすることで、なにか不具合があっても、
「ここの表示、君のイメージと違うみたいだよ」
「ここの処理、君の思うようには動いてないよ」
という感じで、テスト君が教えてくれるということだ。
#なぜテストを書くのか
例えば、複数人でなんかの開発をしてて、
「やったーー!!完成した!!!!」
と喜んでいたとしよう。
その後、誰かが「あ...、ここ手直ししなきゃ。アップデートじゃ!」
という感じで、どこかに手を加えたとする。
すると、「あれ...、こっちがなんか変だぞ?」
とか、「俺の作ったクラスが動かなくなってる」
みたいな、
意図しないエラーがあちこちで発生する事が多い。
要するに、ある箇所のソースコードをちょっと書き換えたせいで、他の関数に影響を与えたりするわけ。
大きいプログラムになればなるほど、そのエラーの原因を突き止めるのは大変だろうし、
最悪、エラーに気づかないままリリースしちゃうこともある。
こうならないために、テストを書く。
正直、テストを書こうとすると結構大変だが、プログラムが膨大になれば、エラー解決のほうがよっぽど大変だろうと思う。
#テストの準備
Laravelに標準搭載されているPHPUnit
を使う。
今回は、app/Http/Controllers/BookController.php
のテストを書くという前提で話を進める。
$ php artisan make:test BookTest
このコマンドを実行するだけで、テスト用のファイルがtests/Feature/
に生成される。
上記のように命名するのが一般的らしい。
#テストを体験してみる
先程生成されたファイルを見てみよう。
<?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.
こんな感じで、もちろんエラーとなる。
#より実践的なテスト
実際に筆者が作ったアプリの一部を切り取って、テストを書いてみよう。
##テスト対象の動作
このように、本のISBNを入力してRegisterボタン
を押すと、
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
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');
}
(省略)
}
<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>
<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()
は、レスポンスが来ていないと使えない。
これらのメソッド以外にも、たくさんのメソッドがある。
利用可能なアサートは公式を参考にしてほしい。
でも正直、今回の事例だと使えるメソッドはこれくらいしかない。
#おわりに
頑張って全てのメソッドに対してテストを書こう...