LoginSignup
4

More than 3 years have passed since last update.

テストから逃げてきたクソザコエンジニアでしたがとうとうテストから逃げられなくなりました。

Last updated at Posted at 2019-04-08

今は昔ロクにテストも書かず自己満足なコードばかり書いてたIT土方ありけり。

実は昔の話じゃない

どうやらヤツは今(執筆時)も関わってる業務でやっとテストを書くようになったらしいです。
多分ヤツ以外にもテストに苦手意識持ってる人いると思うんです。いえ、います。(断言)

ということなので、今回はテストコード書いた際の備忘録として。

テストとは

知らん。
テストってのは、コンピュータのプログラムから仕様にない振舞または欠陥(バグ)を見つけ出す作業のことである。ソフトウェアテストで見つかったプログラム中の欠陥を修正する作業をデバッグという。ソフトウェアテストに成功するとは、テストで欠陥が発見されるか、規定した試験項目にすべて合格するか、規定した品質目標に到達することである。目標とした品質には、規定した試験項目にすべて合格することもある。例えば、OS, プログラミング言語では、仕様を満たしているかどうかの適合試験を規定している。ソフトウェアテストでは、欠陥が存在することを示すことはできるが、欠陥が存在しないことは証明できない。ソフトウェアに仕様にない振舞がないことを保証する作業を証明といい、証明用のシステム、証明しやすい言語も多数存在している。本項では動的なソフトウェアテストを中心に扱うもののことです。
ありがとうwiki
don't it call wiki

冗談はさておき僕の言葉でテストを説明してみます。
書いたプログラムって想定通り動いてるの?ちゃんとI/Oは仕様通り?重大なバグとかない?分岐ループとかの条件は大丈夫?ってのを確認してく作業の事をテストと呼ぶらしいです。
コードレビューとか、偉い人達集めて実際に動かして説明するのもテストに含まれるらしいですが、今回はプログラムを書いて機械にゴリゴリ確認してもらう方の作業を取り扱います。

どれだけ(テスト)がんばりゃいい 誰かのためなの?

ALONE開発だろうと、コードが大きくなれば保守は大変になりますし、複数人でなんてなおさらです。人の書いたテストすら無いウンコードと関わりたい変態(エンジニア)なんて少数じゃないでしょうか?
テストは将来の自分のためにも、周りの開発者のためにもなります。分かっているのに、、、

一応、今回テストの対象になる機能は、
1. DBから1件ずつデータを取得し
2. そのデータを元にとあるエンドポイントにリクエストを投げ
3. レスポンスのステータスコードだったり、ボディだったりを想定するものか判定し
4. 正しければ整形して
5. 保存する。
6. 上記を繰り返す。

てものです。コード全体載せるのは色々とアレなので、テストコード書いて、説明で必要だなと思った部分だけ載せていきます。
そもそも今回の記事自体、テストDTが初体験した結果学んだ知識の共有だったり、感想だったりをつらつら書いて行くだけなので機能の方に重点はおいてるわけでもないですしお寿司。

こっから先が分からねぇんだよ

この記事の本題ですが、そもそもどうやってテストコード書くのかが分かりませんでした。
というのも、色々な記事みても難しくて、 ?・・・・・・・何言ってんだ・・・・・・・?・・・こいつ・・・・状態。
まぁなぜ難しく感じたかって単純に僕の知識不足もあるんですが、調べて出てくる記事は大体記事を書いた人のテストケースについて書かれていることが多く僕のパターンに当てはまるものが見当たらなかったのもあります。
さぁ困った。そういう時よく言われますね。公式ドキュメント読め。ヒントはMockだ。

書いてました。
Laravel5.7 モック
PHPUnit テストダブル
Mockery

なんだこんなに良い情報がたくさん載ってるじゃないか、なんて心強い。これならテストもお茶の子さいさい!!
まずはDBから1件ずつ取得し、の部分をテストしてみましょう。多少テーブル名とかカラム名は変えてますが、ほぼ丸コピです。

target
    private function modelIterator(string $model_name, array $select = ['*'])
    {
        foreach ($model_name::select($select)->cursor() as $data) {
            yield $data;
        }
    }

こちらが対象のコード、テーブル名と取得するカラム名を指定してデータを1件ずつ返すジェネレータ関数です。名前にイテレータって付いてるけど記事書いた後に気がついたし修正するよりもここでジェネレータとイテレータの違いを調べる人が増えるといいなぁと願いを込めてそのままにしておきます。面倒とかではないです
ではお次にこちらがテストコード。

test
<?php
use Mockery as Mock;

class BillingTest extends TestCase
{
    public function test_getRecordValid()
    {
        $mock = Mock::mock('getRecordChecker');

        $target_class = new \App\Hoge($mock);

        $target = new \ReflectionMethod($target_class, 'modelIterator');

        $target->setAccessible(true);

        $result = [];

        foreach ($target->invoke($target_class, 'App\Model', ['id', 'group_id']) as $data) {
            $this->assertIsObject($data);
            $array = json_decode(json_encode($data), true);
            $this->assertArrayHasKey('id', $array);
            $this->assertArrayHasKey('group_id', $array);
        }
    }

    public function test_getRecordInValid()
    {
        $mock = Mock::mock('getRecordChecker');

        $target_class = new \App\Hoge($mock);

        $target = new \ReflectionMethod($target_class, 'modelIterator');

        $target->setAccessible(true);

        $result = [];

        foreach ($target->invoke($target_class, 'App\Model', ['id', 'group_id']) as $data) {
            $this->assertIsObject($data);
            $array = json_decode(json_encode($data), true);
            $this->assertArrayNotHasKey('hoge', $array);
            $this->assertArrayNotHasKey('fuga', $array);
        }
    }
}

targetみて分かるようにテストしたい処理はprivateです。自分自身からしか呼び出せないメソッドもこのように呼び出す事ができるらしいです。
ここがキモ

        $mock = Mock::mock('getRecordChecker'); --A

        $target_class = new \App\Hoge($mock); --B

        $target = new \ReflectionMethod($target_class, 'modelIterator'); --C

        $target->setAccessible(true); --D

mockを生成して(A)、targetインスタンス生成して(B)、そしてココ、ReflectionMethod(C)からsetAccessible(true)(D)の処理によりprivateメソッドを呼べます。呼ぶ時は
$target->invoke($target_class, 'App\Model', ['id', 'group_id'])こんな感じで、、もっと一般化すると
$target->invoke(メソッドを実行するオブジェクト, 引数)って感じです。引数は配列にしなくても第2引数、第3引数と書いていくだけで呼ぶメソッドに渡ります。
テストの中でオブジェクトか判定して、配列にして、配列としてキーを持ってるか(or持ってないか)ってしてますが、もちろんオブジェクトとしてキー(?)を持ってるかどうかも判定できます。今回は返ってくるオブジェクトの型はEloquentオブジェクトだったので、色々と中身面倒なことになってたため一度配列に直して判定してます。初心者ですので(免罪符)何か良さげな方法あれば是非教えてください。そういうの教えていただけるとすごく嬉しいです。

果たしてテストとしての機能はこれで良いのか分かりませんが、次に進みましょう。



エンドポイントにリクエストを投げつける、ですが、ココらへん調べてみても、APIサーバー側のテストだったり、RestletClient・Postmanを使った自動化みたいなのしか見当たりませんでしたので、テストコード書けてないです。許して何でも。
もし、自分自身がクライアントとしてリクエスト投げて結果を受け取るみたいなテスト書く手法みたいなのあれば、こちらも是非教えていただけると助かります。本当に。流石に、テストのために無意味なリクエストをサーバーに投げるのもあまり宜しくないかと思ってて、、

では次、レスポンスを見て判定、の部分です。一応、上記でテスト書けない!って泣き言ほざいてる関数部分で200以外は弾いています。その他にデータがほしい形式化どうか、要するに今回のパターンで行くと取得したデータが200は返ってきてるけど空かどうか、の判定です。空かどうか、といえどリクエストのパラメータ次第で形式が若干違うので、実際の中身は現時点で想定される返却データの全パターンを網羅してますが、以下に書くtargetコードは簡略化してます。

target
    public function isBreak(array $response)
    {
        if ($response === []) {
            return true;
        }
        return false;
    }

記事のために簡略化してるだけです(大事なことなので以下略)
ここのテストコードは書きやすそうです。publicですし、配列が空か(想定する形式かどうか)、の判定だけなんで引数に渡す配列も返り値もとても単純です。

test
    public function test_judgeTrue()
    {
        $mock = Mock::mock('judgeChecker');

        $target = new \App\Fuga($mock);

        $this->assertTrue($target->isBreak([]));
    }

    public function test_judgeFalse()
    {
        $mock = Mock::mock('judgeChecker');

        $target = new \App\Fuga($mock);

        $this->assertFalse($target->isBreak(['foo']));
    }

これで判定のテストコードは書けました(投げやり)
isBreakは処理中断するべきかどうか判定して、中断するならtrueが返ります。trueとfalseが返るであろう値を代入してます。
assertTrue(False)は第1引数がTrue(False)かを判定してます。



で、整形しての部分なんですが、ここもテストコード書きましたが、ここは全体的に載せる事が難しい、というのも載せられない部分を修正してみたら似たようなテストケース載せた方が早い事に気がついたので大体こんな流れで書いた、というののみ載せます。

target
    public function format(array $response)
    {
        //なんか色々な処理
        return ['year' => $response['year'], 'data' => $response['data']];
    }

こんな感じのコードに対して

test
    public function test_validation()
    {
        $mock = Mock::mock('judgeChecker');

        $target = new \App\Fuga($mock);

        $array = $target->format(['year' => '2019', 'data' => ['hoge', 'fuga']]);

        $this->assertArrayNotHasKey('year', $array);
        $this->assertArrayNotHasKey('data', $array);
    }

こんな感じのテストを、、、以上!

ってな感じでひとまずテストコードが書けました。
書いてみて、案外さっくり終わったのと、終わった後は「あぁ、こんなもんだったのか。」となってしまいました。初体験なんてそんなもんです。

実行

後は実行するだけですが、ここで一瞬つまりました。
色んな記事見てもphpunitコマンドで実行できると言われましたが、僕の環境だと
APPLICATION_PATH/vendor/bin/phpunit APPLICATION_PATH/tests/Feature/ExampleTest.php
と絶対パスでphpunitとテストファイルの場所を指定しないと動きませんでした。まぁ結論としては動いたのでオッケーです!

あと最初に書き忘れてたのでココに書きますが、今回はLaravel使ってて、テストコードの生成は
php artisan make:test HogeTest
で生成できます。
閑話休題。

実行してみるとテスト結果が表示されます。

PHPUnit 7.5.3 by Sebastian Bergmann and contributors.

......                                                              6 / 6 (100%)

Time: 2.33 seconds, Memory: 14.00MB

OK (6 tests, 60 assertions)

と、こんな感じで書いた限り全部のテストが通りました!


僕自身文章力も無い中、テストコードを書くのと同時に記事書き進めていったので、所々可笑しな言い回しもありますし、第一僕自身が、今書きたいのはこういうことじゃない!みたいに思いながらも書いてたんですが、この記事が少しでもテストに苦手意識を持つ人の元に届いて、その上でテストが少しでも身近なものに感じてもらえれば幸いです。

つたない文章ですが、ここまで読んでいただきありがとうございました。

追記

ここのコードですが、凄い人からこんなお言葉をいただきました。

target
    private function modelIterator(string $model_name, array $select = ['*'])
    {
        foreach ($model_name::select($select)->cursor() as $data) {
            yield $data;
        }
    }

スクリーンショット 2019-04-09 15.01.36.png

その通りです誠に申しわけございませんでした!!!!

Eloquent継承したオレオレモデル作って、そこに書いておけばこの返り値のタイプヒントまで書けてテストコードも簡略化されますし、これに近い実装をするならそのやり方を取った方が適切と考えます。。
現在、返り値のタイプヒントはobjectである事しか明記出来ませんが、オレオレモデルに書いて各モデルに実装すれば、このメソッドはそのEloquentオブジェクトしか返さないと指定出来ますし、引数も第1引数はいらなくなります。

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
4