入社して2ヶ月の超初心者が、Laravelテストでつまずいた点を話します

Laravelテスト記事のサムネイル画像

こんにちは!
ニジボックスに7月末より入社している@tadoumaです。

エンジニアになって2ヶ月、テストを書き出して1ヶ月半、
超初心者の私がLaravelテストを書いてつまずいた点を書いてみます。

こんなの知ってるよ!となるかもしれませんが、、

温かい目で見守ってください。

自己紹介

・エンジニアになって2ヶ月の超初心者(文系出身で、プログラミングに触れた経験ほぼなし)
・前職は営業
・もちろんテスト書くのも初めて
・猫が好き

それでは、早速まとめてみようと思います!

目次

1.assertSee/assertDontSee
2.リダイレクト後のテスト
3.フィールド名の変更とテストの書き方
4.テスト名とアノテーション
5.Factory、RefreshDatabase、IDオートインクリメント
6.最後に

それでは、1つずつ見ていきましょう!

1.assertSee/assertDontSee

php:AssertSee.php
$res = $this->get("url");
$res->assertStatus(200)
    ->assertDontSee('ネコちゃん');

仕様変更に伴い、このテストが通らなくなりました。

状況

・変更前
    if文で分岐し、条件Aに当てはまるユーザーには「ネコちゃん」を表示。
    上記のテストでは、条件Aに当てはまらないユーザーの時、
    「ネコちゃん」が出ないことを確認していた。

・変更後
    if文ではない箇所で、「ネコちゃん」という文面が追加された。
    そのため、どんな条件のユーザーでも「ネコちゃん」が出るようになった。

どうにかして、if文で出し分けている方の「ネコちゃん」だけが
見えていないことを確認したい。
ただ、

assertDontSee('ネコちゃん') にすると、通らない。

解決策

$res = $this->get("url");
$res->assertStatus("200")
    ->assertDontSee('<li>ネコちゃん</li>')
    ->assertSee('ネコちゃん');

if文の方の「ネコちゃん」は、liタグに囲まれているため、
これを含めてassertDontSeeで調べると、うまくいきました!

なぜ?

下記はテストを実行し、エラーが出てきた画面。
if文で出し分けているほうが、<li>ネコちゃん</li>です。

<div class="fuga">ネコちゃん</div<\n

<div class="hoge">\n
    <li>ネコちゃん</li>\n
</div>\n

Laravelドキュメントを見ると、assertSeeassertDontSeeの定義は、
「指定したテキストが、ページ上に存在することを宣言します。」と書いてあります。

ページ上に存在する というのが何を表しているのか分からなかったのですが、
どうやらレスポンスとして返ってきたHTMLソースを見ているようでした。
そのため、タグを含めて調べるとうまくいきました。

ちなみに・・・

上記のような場面以外でも、
例えば想定結果が「1」の時、assertSee("1")なんかやっても
「1」なんかどこにでもありますよね。

そういった場合は下記のように、表示されている前後の文字列も含めて書いてあげると、
正確なAssertSee/assertDontSeeができます。

//$hogeNumberはある処理の結果。今回は1の想定
$res = $this->get("url");
$res->assertSee("結果は".$hogeNumber."件です");

2.リダイレクト後のテスト

バリデーションエラーの文言がきちんと出ているか調べるため、
リダイレクト後にassertSeeが効いているか見ようとしました。
最初に書いたコードがこちら。

$res->assertRedirect('url')
   ->assertSee("内容を入力してください。");

これを実行すると、下記のようにエラーが返ってきました。

Failed asserting that '<!DOCTYPE html>\n
<html>\n
    <head>\n
        <meta charset="UTF-8" />\n
        <meta http-equiv="refresh" content="0;url=http://url" />\n
\n
        <title>Redirecting to http://url</title>\n
    </head>\n
    <body>\n
        Redirecting to <a href="http://url">http://url</a>.\n
    </body>\n
</html>' contains "内容を入力してください。".

↓かなり省略して書くと、
Failed asserting that '(ここにレスポンス)'contains "内容を入力してください。".
=つまり、レスポンスの中に「内容を入力してください。」という文字が入っていない、と言っています。

何が返っているのか?

なぜ上記だとできないのか。
リダイレクト処理では何が返ってくるのか、が鍵になります。

リダイレクトとはつまり・・・

HTTPヘッダにあるHTTPステータスコードにてリダイレクトの種類を伝え、Location:ヘッダで移動先を伝える

というもの。
つまり、リダイレクトして直接assertSeeしても、
リダイレクトのレスポンス(≒移動先を伝えているところ)しか見ていない、ということになります。

解決策

$res = $this->post('url',['content' => '']); 
$res->assertRedirect('url2');
$this->get('url2')
    ->assertSee("内容を入力してください。");

こうすれば、リダイレクト後にgetしていることになるので、
バリデーション文言がしっかり取れます!

3.フィールド名の変更とテストの書き方

$res = $this->post('url', [
            'content' => 'ニャ〜〜〜ン',
        ]);
$this->assertEquals("1", Hoge::count());

上記は、postした内容がきちんとDBに反映されているか見ているテストです。
ある時、フィールド名を変更したところ、上記がエラーになってしまいました。

フィールド名の変更は下記の通りです。

変更前

<textarea name="content">

変更後

<textarea name="hoge[content]">

※今回、hogeにのみ適用したいバリデーションルールがあったため、
このような実装にしました。

解決策

$res = $this->post('url', [
    'hoge' => [
        'content' => 'ニャ〜〜〜ン',
    ]
]);
$this->assertEquals("1", Hoge::count());

この場合、hoge[content]と渡すようになったので、
テストでpostする際もこの形で書いてあげる必要がありました。

当たり前なのですが、急にテストでエラーが出ると最初は戸惑いました。

4.テスト名とアノテーション

みなさん、テストのメソッド名を日本語で書けることはご存知でしょうか?
イメージはこんな感じ。

public function ステータステスト()
{
    $res = $this->get("url");
    $res->assertStatus(200);
}

ただ、このままだとテストとして認識されません。
テストとして認識してもらうには、下記の宣言が必要となります。


/** * @test */

つまり…。

/**
 * @test
 */
public function ステータステスト()
{
    $res = $this->get("url");
    $res->assertStatus(200);
}

こう書くとうまくいきます。

これはアノテーションと言って、
かなりざっくり言うと

・「注釈」 という意味
・/** で始めて */ で終わる
・テスト実行時の振る舞いを決める

詳しくはPHPUnitのドキュメントへ。

このアノテーションを使うと、英語のテスト名の際、
Testと含まなくてもテストだと認識されます。

逆に言うと、このルールを知らずに
「日本語名のテスト追加したら、通った!」
「Testを含まないメソッド名のテスト追加したら、通った!」
と思っていたら…。

スクリーンショット 2018-10-01 21.51.45.png

実際はテストと認識されていない(※↑テスト数が増えていない)だけだった…!なんてことも。
(私は実際やらかして泣きそうになりました)

ちなみに

他にもたくさんアノテーションはありますが、個人的には@groupが便利でした。

@groupとは、指定したテストのみを実行してくれるアノテーションで、
使う際はこのように書きます。
今回は、グループ名をtestgroupとします。(ここはなんでもOK)

/**
 * @group testgroup
 */
public function hogeTest()
{
   //処理を書く
}

そして、実行時は下記のように書けばOK。
テストのファイルパス --group グループ名
値を入れるとこんな感じ。(あくまでイメージです。ファイルパスは環境によって異なります)
./phpunit tests/GroupTest.php --group testgroup

ここだけでエラーが起こってるんだ!といった場合や、
このテストだけ検証したいんだ!という場合にとても便利でした。

5.Factory、RefreshDatabase、IDオートインクリメント

下記はつまずいたというより、やっていく中で発見したものです。

結論を言うと、
・RefreshDatabaseを使っても、IDのオートインクリメントはリセットされない。
ということを発見しました。

それでは、少しずつ見ていきます。

RefreshDatabaseとは?

各テスト(メソッド)の後にDBをリセットしてくれるトレイトです。
(詳細はドキュメントにて)
つまり、毎回テストした後、DBをまっさらにしてくれるので
DBの影響を受けずにテストを行うことができます。

使い方は、下記を書いてあげるとOKです。

use Illuminate\Foundation\Testing\RefreshDatabase;

class HogeTest extends TestCase
{
    use RefreshDatabase;

    public function testFuga()
    {
         //テストを書いていく
     }
}

Factoryとは?

Factoryを用いて、テスト用の疑似データを作ることができます。
使い方は下記のとおり。

$user = factory(User::class)->create();

もし、カラム名を指定してデータを入れたいのであれば、

$user = factory(User::class)->create([
             'name' => 'てすとネコちゃん',
             'email' => 'test_nyan@example.com'
        ]);

このように指定してデータを作成することができます。

ここで指定していないカラムは、factoryを作る際にできた
/database/factories/以下の○○Factory.phpファイルの中に
初期値を指定してあげましょう。
(今回はUserFactoryになっています。)

<?php

use Faker\Generator as Faker;

$factory->define(App\Daos\User::class, function (Faker $faker) {
    return [
        'name' => $faker->name,
        'age' => '22',
        'email' => 'firstmail@example.com',
    ];
});

この場合、ageは先程指定していなかったので、
このファイルで指定した初期値「22」が適用されます。
(ちなみにnameで使用している$fakerは、
任意のダミーデータを作成してくれるものです。)

オートインクリメントの関係

それでは、ざっくり説明します。

public function hogeTest()
{
   $users = factory(User::class ,2)->create();
   var_dump($users[0]->id); //1
   var_dump($users[1]->id); //2
   var_dump($users->count()); //2
}

public function fugaTest()
{
   $users = factory(User::class ,2)->create();
   var_dump($users[0]->id); //3
   var_dump($users[1]->id); //4
   var_dump($users->count()); //2
}

上記を見ればわかるように、

・RefreshDatabaseのおかげでメソッドごとにDBはリセットされるため、
count()の結果は常に2になる

・ただし、DBのオートインクリメントはリセットされないため、
IDは連番になっている。

ということが分かります。

つまり、(冒頭でも述べましたが)
・RefreshDatabaseを使っても、オートインクリメントはリセットされない。
ということを発見しました。

「だから?」という感じではあるのですが、、、
意外と周りにも知らない人がいたので書いてみました。

6.最後に

入社して2ヶ月、テスト始めて1ヶ月半。
全く何もわからなくって、最初の頃は家で号泣(悔し泣き)したりしていましたが、
振り返ってみると本当に本当に学びが多かったと感じます。

厳しくも優しく接してくださった@niisan-tokyoさん、
いつも女神のように優しい(男性の)@fudoさん
本当にありがとうございました!

初心者目線で「え〜なんで〜!?」と思ったところをまとめた記事でしたが、いかがでしたでしょうか。
私と同じような初心者の方には、少しでも参考になればいいなと思いますし、
経験者の方には「初心者にはこう見えているんだな〜」という参考になれば、と思っています。

読んでいただきありがとうございました!

RECRUIT

私たちと一緒に
「魂を込めたモノづくり」をしませんか?
ニジボックスではクリエイターを
随時募集しています。
気軽にお問い合わせください。