株式会社クイックのWebサービス開発blog

HAPPYなサービスプランナー・エンジニア・デザイナーのブログです。

テストしやすいコードについて考えてみた

こんにちは🍣🍶です。

テストって書くの難しいですよね。
どうしたらテストを簡単にできるのか考えてみました。

ちなみに前回はPHP Unitでテストの書き方について紹介させていただきましたのでテストの書き方についてはこちらをご覧ください! aimstogeek.hatenablog.com

まずはどんな関数がテストしやすいか見てみましょう。

<?php
function a($a) {
    if($a === 1) return 'a';
    if($a === 2) return 'b';
    return 'c';
}

これならテストするのは簡単ですよね?

一番テストしやすい形は副作用がなく入出力がはっきりした関数です。 同じ引数なら同じ値を返し、呼んだ後に影響が残らないものです。

でも実際のアプリではなかなかこんな単純な形にはなりませんよね。 そこでテストを難しくしてしまう要素をいくつか見ながら対処法を考えていきます。

グローバルに状態を持っている

<?php
class A {
    private static int $count = 0;
    
    public function a() {
        self::$count += 1;
        return self::$count;
    }
}

この関数の問題点はA::$countという関数を呼んだ側がコントロール出来ない不安定な入力があるということです。

function aのテストをしようとしても呼ばれる度に戻り値が変わるし、A::$countが何処かで変更されてしまっても結果が変わるためテストがしにくいです。

グローバル変数は原則として使わないようにしましょう。

外部要因を含む

テスト対象がアプリ内に留まらない場合難易度が跳ね上がります 例えば何かの処理をするメソッドでログを書く必要があったとします

<?php
    
function a($param) {
    // do something
    Logger::info("hogehoge");
    return $result;
}

例えばログがファイルに書き込まれるとした場合この関数のテストでは実際にファイルに"hogehoge" と書き込まれているかの確認が必要になってしまいます。 つまりこの関数には副作用があるということです。

なので以下のように書き換えてみましょう

<?php

interface Logger {
    public function info(string $messge)
}

    
function a($param, Logger $logger) {
    // do something
    $logger->info("hogehoge");
    return $result;
}

こうすればこの関数から直接の副作用が無くなります。 なのでテストの際にLoggerのmockを渡せばログ実際にファイルの確認をしなくてもテストができます。 実際にファイルに書き込まれるかなどの確認は何度Loggerを使っても interface Loggerの実装のテスト以外では不要になります。

ファイルIOやDBアクセス、ネットワークを介したやりとり、現在時刻、環境変数などのアプリで完全にコントロール出来ないものはなるべく直接使わないようにしましょう。

複数のことを一つの関数で行う

このように複雑な条件と実際の処理を同じ関数で行うとどうなるでしょうか?

<?php
function doSomething($a, $b) {
    // 何の意味もない複雑な判定処理
    if($a + $b = 5 && $b === 1 || $a === 2 && $b === 2) return 0;    
    return $a + $b;
}

この場合、ifの判定処理とdoSomething1が機能しているかのテストをする必要があります。

しかも条件判定のみのテストをしようとしても、このままだと実際どちらの判定になったのかを テストで把握するのも手間がかかりそうです。

試しに下のように判定処理を切り出してみます。

<?php

function isHoge($a, $b) {
    return $a + $b = 5 && $b === 1 || $a === 2 && $b === 2);
}

function doSomething($a, $b) {
    if(isHoge($a, $b)) return 0;    
    return $a + $b;
}

こうすることで条件判定のテストと実際の処理内容のテストを分けることができました。 また、条件に名前をつけることが出来て可読性も上がります。

判定処理のテストは関数isHogeで行え、単純なテストが作れますし、 関数doSomethingのテストは実際の処理内容だけを気にすれば良くなりました。

また、関数名を見れば何を判定しているのか分かるようにもなり可読性も上がります。

関数が複雑になりすぎているかはテストの書きやすさやテストの名前を考えてみると分かりやすいです。

例えば修正後の関数のテストは以下のような入力と出力の組み合わせだけで可能ですし、何をテストしているのか分かりやすいですよね。

// isHoge
{ input: [2, 2], output: true }
{ input: [4, 1], output: true }
{ input: [3, 1], output: false }
{ input: [5, 1], output: false }
{ input: [1, 2], output: false }


// doSomething
{ input: [1, 1], output: 0 } // isHogeがfalseのパターンは一つで済む
{ input: [3, 1], output: 4 }
{ input: [5, 1], output: 6 }
{ input: [1, 2], output: 3 }

修正前の場合だと分岐条件のテスト、分岐がtrueだった場合のテストとfalseだった場合のテストが必要ですよね。 条件の数だけ掛け算で増えていくので分かりにくいと思ったら関数を分割してしまいましょう。

また、後からそのテストをみた人が簡単に理解できるかも重要です。

テストが〜かつ〜かつ〜で〜の時〜になり〜が〜することを確認するのように長いと どうなれば良いのか分かりにくいですし機能の変更をした際にテストを修正するのも大変です。

少しでも分割した方が良いと感じたら分割してしまいましょう。

最後に

テストしやすいコード

  • 関数の入力がコントロールできる
  • 関数の出力が入力によって完全に決まる
  • 外に影響を及ぼさない

テストしにくいコード

  • 関数の入力が変更しにくい
  • 関数の出力が把握しにくい
  • 外部に影響を及ぼす
  • 複数のことを行なっている

テストを書きやすくする方法は色々ありますし絶対にこれが正解ということはありません どうすればテストしやすいか考えながらコーディングしたいですね。

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


\\『真のユーザーファーストでマーケットを創造する』仲間を募集中です!! //

919.jp