14章 - TKBTK48/ReadableCode GitHub Wiki

第14章 テストと読みやすさ

14.1 テストを読みやすくて保守しやすいものにする

テストコードというのは「本物のコードの動作と使い方を示した非公式な文書」だと考えるプログラマもいるほどである。

鍵となる考え : 他のプログラマが安心してテストの追加や変更が出来るように、テストコードを読みやすくする。

テストコードが大きくて恐ろしいものだとしたら、以下のようなことが起きる。

  • 本物のコードを修正するのを恐れる。

  • 新しいコードを書いたときにテストを追加しなくなる。

14.2 このテストのどこがダメなの?

検索結果のスコアをソートしてフィルタする関数がある。

// 'docs'をスコアでソートする(降順)。マイナスのスコアは削除する
void SortAndFilterDocs(vector<ScoredDocument>* docs);

最初のテストは以下のようになっていた。このテストコードには少なくとも8つの問題がある。本章が終わるまでに全部見つけて修正する。

void Test1() {
  vector<ScoredDocument> docs;
  docs.resize(5);
  docs[0].url = "http://example.com";
  docs[0].score = -5.0;
  docs[1].url = "http://example.com";
  docs[1].score = 1;
  docs[2].url = "http://example.com";
  docs[2].score = 4;
  docs[3].url = "http://example.com";
  docs[3].score = -99998.7;
  docs[4].url = "http://example.com";
  docs[4].score = 3.0;

  SortAndFilterDocs(&docs);

  assert(docs.size() == 3);
  assert(docs[0].score == 4);
  assert(docs[1].score == 3.0);
  assert(docs[2].score == 1);

14.3 テストを読みやすくする

設計原則として、 大切ではない詳細はユーザから隠し、大切な詳細は目立つようにする

上記のコードでは、「vectorの設定」が一番目立っている。

これをきれいにするためには、設定用のヘルパー関数を作る。

void MakeScoredDoc(ScoredDocument* sd, double score, string url) {
    sd->score = score;
    sd->url   = url;
}
void Test1() {
    vector<ScoredDocument> docs;
    docs.resize(5);
    MakeScoredDoc(&docs[0], -5.0, "http://example.com");
    MakeScoredDoc(&docs[1], 1, "http://example.com");
    MakeScoredDoc(&docs[2], 4, "http://example.com");
    MakeScoredDoc(&docs[3], -99998.7, "http://example.com");
    ...
}

これでも"http://example.com" が目立ちすぎていて目障りである。

あとは、docs.resize(5)や、&docs[0]や&docs[1]などが邪魔である。

ヘルパー関数を修正して、AddScoredDoc()という名前に変える。

void AddScoredDoc(vector<ScoredDocument>&docs, double score) {
    ScoredDocument sd;
    sd.score = score;
    sd.url = "http://example.com";
    docs.push_back(sd);
}
void Test1() {
    vector<ScoredDocument> docs;
    docs.resize(5);
    AddScoredDoc(&docs, -5.0);
    AddScoredDoc(&docs, 1);
    AddScoredDoc(&docs, 4);
    AddScoredDoc(&docs, -99998.7);
    ...
}

最小のテストを作る

このテストコードを改善するためには、「第12章 コードに思いを込める」の技法を使う。

このテストが何をしようとしているのかを簡潔な言葉で説明する。

文書のスコアは[-5, 1, 4, -99998.7, 3]である。 SortAndFilterDocs()を呼び出した後のスコアは[4, 3, 1]である。 スコアはこの順番でなければならない。

テストコードは以下のようになっているとよいだろう。

CheckScoresBeforeAfter("-5, 1, 4, -9999.7, 3", "4, 3, 1");

独自の「ミニ言語」を設計する

最近のC++では、以下のように配列リテラルをそのまま引数として渡せるようになっている。

CheckScoresBeforeAfter({-5, 1, 4, -9999.7, 3}, {4, 3, 1});

14.4 エラーメッセージを読みやすくする

先程のコードにおいて、assert(output == expected_output) が失敗したら、そのコードを追いかけるのは大変になる。 Boost C++ライブラリを使って、

assert (output == expected_output);

を以下のように書き換える。

BOOST_REQUIRE_EQUAL(output, expected_output);

手作りのエラーメッセージ

BOOST_REQUIRE_EQUAL()をよりもっといい感じにしたい。エラーメッセージが欲しいなら、自分で書けばいい!

疑問:今回の読売のシステム更新ではここまで手厚いテストコードはあまり見ない。普通はどこまでやるものなのか

14.5 テストの適切な入力値を選択する

今テストに使っている値はでたらめだ。

CheckScoresBeforeAfter("-5, 1, 4, -9999.7, 3", "4, 3, 1");

鍵となる考え : コードを完全にテストするもっとも単純な入力値の組み合わせを選択しなければならない。

入力値を単純化する

テストには最も綺麗で単純な値を選ぶ

1つの機能に複数のテスト

小さなテストを複数作る方が、簡単で、効率的で、読みやすい。 複数のテストで別々の方向からバグを見つけ出すようにする。

例えば、SortAndFilterDocs()には4つのテストがある。

CheckScoresBeforeAfter("2, 1, 3", "3, 2, 1");   // ソート
CheckScoresBeforeAfter("0, -0.1, -10", "0");    // マイナスは削除
CheckScoresBeforeAfter("1, -2, 1, -2", "1, 1"); // 重複は許可
CheckScoresBeforeAfter("", "");                 // 空の入力は許可

14.6 テストの機能に名前を付ける

SortAndFilterDocs()をテストするコードは、Test1()に入れていたが、名前はナンセンスである。

以下のことをすぐに理解できるものが良い。

  • テストするクラス(もしあれば)
  • テストする関数
  • テストする状況やバグ
void Test_SortAndFilterDocs() {
    ...
}

次に、状況に応じてテスト関数を分割するかどうかを考える。

分割する場合にはTest_<関数名>_<状況>()という形式にすればよい。

void Test_SortAndFilterDocs_BasicSorting() {
    ...
}
void Test_SortAndFilterDocs_NegativeValues() {
    ...
}

14.7 このテストのどこがダメだったのか?

  • テストが何をしているかを1つの文で記述した方がよい。
  • テストが簡単に追加できない。
  • 失敗メッセージが役に立たない。
  • 一度にすべてのことをテストしようとしている。
  • テストの入力値が単純ではない。
  • テストの入力値が不完全である。
  • 極端な入力値を使ってテストをしていない。
  • Test1()という意味のない名前がついている。

14.8 テストに易しい開発

テストしやすいようにコードを設計するようになるのだ! テストに易しい設計をすれば、振る舞いごとにうまく分割されて、自然にコードが構成されていく。

テスト容易性の低いコードの特性とそこから生じる設計の問題

  • グローバル変数を使っている。 グローバルの状態をテストごとに初期化する必要がある。
  • 多くの外部コンポーネントに依存している。 最初に足場を設定しなければいけないので、テストを書くのが難しい。
  • コードが非決定的な動作をする。 テストがあてにならず、信頼できない。

テスト容易性の高いコードの特性とそこから生じる設計の利点

  • タスクが小さい。あるいは内部状態を持たない。 テストがしやすい。メソッドをテストするのにセットアップがあまり必要にならない。
  • クラスや関数が1つのことをしている。 完全にテストをするためのテストケースが少なくて済む。
  • クラスは他のクラスにあまり依存していない。高度に疎結合化されている。 各クラスは独立してテストできる。
  • 関数は単純でインタフェースが明確である。 明確な動作をテストできる。

14.9 やりすぎ

テストのために本物のコードの読みやすさを犠牲にしてしまう。

テストは読みやすくする。テストをしやすくするために、本物のコードにごみを入れてはならない。

テストのカバレッジを100%にしないと気が済まない

コードの90%をテストする方が、残り10%をテストするよりも楽である。

現実的に、カバレッジが100%になることはない。もしも100%になっているのだとしたら、バグを見逃しているか、機能を実装していないか、仕様が変更されていることに気づいていないのどれかである。

テストがプロダクト開発の邪魔になる

プロジェクトの一部に過ぎないテストが、プロジェクト全体を支配しているような状況は避けるべきである。

14.10 まとめ

  • テストのトップレベルはできるだけ簡潔にする。入出力のテストはコード1行で記述できるとよい。
  • テストが失敗したらバグの発見や習性がしやすいようなエラーメッセージを表示する。
  • テストに有効なもっとも単純な入力値を使う。
  • テスト関数に説明的な名前を付けて、何をテストしているのかを明らかにする。Test1()ではなく、Test_<関数名>_<状況>のような名前にする。
⚠️ **GitHub.com Fallback** ⚠️