14章 - TKBTK48/ReadableCode GitHub Wiki
テストコードというのは「本物のコードの動作と使い方を示した非公式な文書」だと考えるプログラマもいるほどである。
鍵となる考え : 他のプログラマが安心してテストの追加や変更が出来るように、テストコードを読みやすくする。
テストコードが大きくて恐ろしいものだとしたら、以下のようなことが起きる。
-
本物のコードを修正するのを恐れる。
-
新しいコードを書いたときにテストを追加しなくなる。
検索結果のスコアをソートしてフィルタする関数がある。
// '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);
設計原則として、 大切ではない詳細はユーザから隠し、大切な詳細は目立つようにする
上記のコードでは、「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});
先程のコードにおいて、assert(output == expected_output) が失敗したら、そのコードを追いかけるのは大変になる。 Boost C++ライブラリを使って、
assert (output == expected_output);
を以下のように書き換える。
BOOST_REQUIRE_EQUAL(output, expected_output);
BOOST_REQUIRE_EQUAL()をよりもっといい感じにしたい。エラーメッセージが欲しいなら、自分で書けばいい!
疑問:今回の読売のシステム更新ではここまで手厚いテストコードはあまり見ない。普通はどこまでやるものなのか
今テストに使っている値はでたらめだ。
CheckScoresBeforeAfter("-5, 1, 4, -9999.7, 3", "4, 3, 1");
鍵となる考え : コードを完全にテストするもっとも単純な入力値の組み合わせを選択しなければならない。
テストには最も綺麗で単純な値を選ぶ
小さなテストを複数作る方が、簡単で、効率的で、読みやすい。 複数のテストで別々の方向からバグを見つけ出すようにする。
例えば、SortAndFilterDocs()には4つのテストがある。
CheckScoresBeforeAfter("2, 1, 3", "3, 2, 1"); // ソート
CheckScoresBeforeAfter("0, -0.1, -10", "0"); // マイナスは削除
CheckScoresBeforeAfter("1, -2, 1, -2", "1, 1"); // 重複は許可
CheckScoresBeforeAfter("", ""); // 空の入力は許可
SortAndFilterDocs()をテストするコードは、Test1()に入れていたが、名前はナンセンスである。
以下のことをすぐに理解できるものが良い。
- テストするクラス(もしあれば)
- テストする関数
- テストする状況やバグ
void Test_SortAndFilterDocs() {
...
}
次に、状況に応じてテスト関数を分割するかどうかを考える。
分割する場合にはTest_<関数名>_<状況>()という形式にすればよい。
void Test_SortAndFilterDocs_BasicSorting() {
...
}
void Test_SortAndFilterDocs_NegativeValues() {
...
}
- テストが何をしているかを1つの文で記述した方がよい。
- テストが簡単に追加できない。
- 失敗メッセージが役に立たない。
- 一度にすべてのことをテストしようとしている。
- テストの入力値が単純ではない。
- テストの入力値が不完全である。
- 極端な入力値を使ってテストをしていない。
- Test1()という意味のない名前がついている。
テストしやすいようにコードを設計するようになるのだ! テストに易しい設計をすれば、振る舞いごとにうまく分割されて、自然にコードが構成されていく。
- グローバル変数を使っている。 グローバルの状態をテストごとに初期化する必要がある。
- 多くの外部コンポーネントに依存している。 最初に足場を設定しなければいけないので、テストを書くのが難しい。
- コードが非決定的な動作をする。 テストがあてにならず、信頼できない。
- タスクが小さい。あるいは内部状態を持たない。 テストがしやすい。メソッドをテストするのにセットアップがあまり必要にならない。
- クラスや関数が1つのことをしている。 完全にテストをするためのテストケースが少なくて済む。
- クラスは他のクラスにあまり依存していない。高度に疎結合化されている。 各クラスは独立してテストできる。
- 関数は単純でインタフェースが明確である。 明確な動作をテストできる。
テストは読みやすくする。テストをしやすくするために、本物のコードにごみを入れてはならない。
コードの90%をテストする方が、残り10%をテストするよりも楽である。
現実的に、カバレッジが100%になることはない。もしも100%になっているのだとしたら、バグを見逃しているか、機能を実装していないか、仕様が変更されていることに気づいていないのどれかである。
プロジェクトの一部に過ぎないテストが、プロジェクト全体を支配しているような状況は避けるべきである。
- テストのトップレベルはできるだけ簡潔にする。入出力のテストはコード1行で記述できるとよい。
- テストが失敗したらバグの発見や習性がしやすいようなエラーメッセージを表示する。
- テストに有効なもっとも単純な入力値を使う。
- テスト関数に説明的な名前を付けて、何をテストしているのかを明らかにする。Test1()ではなく、Test_<関数名>_<状況>のような名前にする。