ブログトップ 記事一覧 ログイン 無料ブログ開設

Strategic Choice

2014-11-26

[]テストコード:テスト名はテスト機能名

どういうこと?

テスト関数は、テストする「メソッド」や「状況」でひとまとめにします。そして、その名前は以下のようにします。

Test_<対象メソッド名>_<状況>()

どうして?

テスト関数には、テストの内容を表した名前をつけるべきです。テストコードを読む人が、以下のことをすぐに理解できるようにするためです。

  • テストするクラス
  • テストする関数
  • テストする状況やバグ

どうすれば?

テスト関数名は、テスト機能をうまく表現するように命名します。

例えば、「SortAndFilterDocs」というメソッドのテスト関数であれば、まずは「Test_」という接頭辞をつけて情報をひとまとめにします。

void Test_SortAndFilterDocs() {
	...
}

次に、状況に応じてこのテスト関数を分割するかどうかを考えます。分割する場合は、「Test_<関数名>_<状況>()」という形式にします。

void Test_SortAndFilterDocs_BasicSorting() {
	...
}

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

長くて変な名前にならないかと怖がることはありません。他のコードから呼び出されるものではないので、長くなっても構いません。テスト関数の名前はコメントだと思えばいいのです。

ほとんどのテスティングフレームワークでは、テストが失敗したらその関数の名前が印字されるようになっています。だから、名前は説明的なほうが役に立ちます。

また、テストコードのヘルパー関数の名前は、アサートを使っているかどうかで分けるようにします。たとえば、「assert() を呼び出しているヘルパー関数は、すべて『Check...()』という名前にして、その他は普通のヘルパー関数のような名前にする」などのルールが考えられます。

2014-11-25

[]テストコード:1つの機能に複数のテスト

どういうこと?

テストの入力値は、コードを検証する「完ぺきな値」を1つ作るのではなく、小さなテストを複数作るようにします。

どうして?

小さなテストを複数作るほうが、簡単で、効果的で、読みやすいテストになります。

どうすれば?

小さなテストを複数作ります。複数のテストで、別々の方向からバグを見つけ出すようにします。

例えば、「SortAndFilterDocs()」関数のテストを考えます。

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

「SortAndFilterDocs()」には4パターンのテストがありそうなので、それぞれテストします。

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

もっと丁寧にやる場合は、テストケース(テスト関数)を増やすこともできます。テストケースが分割されていれば、次の人がコードを扱いやすくなります。意図せずにバグを発生させたとしても、失敗したテストによってその場所がわかるからです。

2014-11-21

[]テストコード:入力値を単純化する

どういうこと?

コードを完全にテストする、最も単純な入力値の組み合わせを選択します。

どうして?

適切な入力値とは、「コードを完全にテストするもの」でありながら、「簡単に読めるような単純なもの」であるべきです。ただし、このバランスは、意識しないと保てません。

例えば、以下のように「簡単に読める」テストを書いたとします。

CheckScoresBeforeAfter("1, 2, 3", "3, 2, 1");

このテストは単純です。しかし、「マイナスのスコアをフィルタする」という動作がテストできていません。この部分にバグがあったら、この入力値では検知できません。

では、以下のようにマイナス値を含むテストを書きます。

CheckScoresBeforeAfter("123014, -1082342, 823423, 234205, -235235","823423, 234205, 123014");

今度は必要以上に複雑で、読みにくいテストになってしまっています。

どうすれば?

テストの入力には、最も綺麗で単純な値を選びます。

以下の例で具体的に考えます。

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

最初に目につくのは「大音量」の値「-99998.7」です。これは単に「任意のマイナス値」を表しているだけです。単純化すれば「-1」になります。仮に、「-99998.7」が「ものすごいマイナス値」を表しているのであれば、「-1e100」のような簡潔な値にします。

その他の値についても、もっと単純な数値にできます。マイナスをテストする値は、1つだけあればよさそうです。この点を鑑みて、テストを改善します。

CheckScoresBeforeAfter("1, 2, -1, 3", "3, 2, 1");

テストの効果を変えずに、値の変更だけで、テストをシンプルにすることができました。

2014-11-20

[]テストコード:エラーメッセージをわかりやすく

どういうこと?

テストコードそのものだけではなく、テストコードが出力するエラーメッセージもわかりやすくします。

どうして?

テストに失敗した時のメッセージがわかりにくいと、実装のリズムが狂ってしまいます。

例えば、テストで「assert(output == expected_output)」が失敗して、以下のようなエラーメッセージが表示されたとします。

Assertion failed: (output == expected_output),

function CheckScoresBeforeAfter, file test.cc, line 37.

このエラーを見ても、「output」や「expected_output」の値がわからないので、何をどう直していいかわかりません。

どうすれば?

テスト失敗時のエラーメッセージを、わかりやすく出力します。

まず考えられる方法が、よりよい「assert()」を使うことです。多くの言語やライブラリには、洗練されたassert() が用意されています。

例えば、「Boost C++ ライブラリ」を使うと、「assert(output == expected_output)」は、「BOOST_REQUIRE_EQUAL(output, expected_output)」のように書くことになり、テストが失敗すると、以下のような詳細なメッセージが表示されます。

test.cc(37): fatal error in "CheckScoresBeforeAfter": critical check

output == expected_output failed "1, 3, 4" != "4, 3, 1"]

値が表示され、わかりやすくなっています。ただ、まだわかりにくい部分もあります。理想は、以下のようなエラーメッセージです。

CheckScoresBeforeAfter() failed,

Input: "-5, 1, 4, -99998.7, 3"

Expected Output: "4, 3, 1"

Actual Output: "1, 3, 4"

こういうエラーメッセージが必要な場合、希望に沿うように「自分で書く」という方法があります。

void CheckScoresBeforeAfter(...) {
	...
	if (output != expected_output) {
		cerr << "CheckScoresBeforeAfter() failed," << endl;
		cerr << "Input: \"" << input << "\"" << endl;
		cerr << "Expected Output: \"" << expected_output << "\"" << endl;
		cerr << "Actual Output: \"" << output << "\"" << endl;
		abort();
	}
}

「エラーメッセージはできるだけ役に立つようにする」ということを実現するには、結局、「(自分好みのエラーメッセージを印字する)手作りのアサート」を用意するのが最善の道の一つです。

2014-11-19

[]テストコード:テストの本質を表現する

どういうこと?

テストの本質は、「こういう『状況』と『入力』から、こういう『振る舞い』と『出力』を期待する」と1行で要約できます。この要約を短いコードで表現するため、独自の「ミニ言語」を定義します。

どうして?

コードを簡潔に読みやすくするだけでなく、ステートメントを短くすることで、テストケースの追加が簡単になります。

独自のミニ言語を定義すれば、小さな領域で多くの情報を表現できます。例えば、「printf()」や「正規表現」は、文字列で別の意味を表現できるようになっています。

どうすれば?

「独自のミニ言語」を実装して、テストの本質を表現します。

以下で、具体的に考えてみます。テストコードが以下のようになっています。

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;
	AddScoredDoc(docs, -5.0);
	AddScoredDoc(docs, 1);
	AddScoredDoc(docs, 4);
	AddScoredDoc(docs, -99998.7);

	SortAndFilterDocs(&docs);

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

まだ「楽に読み書きできる」テストにはなっていません。新しい文書の一覧をテストしようと思ったら、コードを大量にコピペしなければいけないからです。

手始めに、このテストが何をしようとしているのかを簡単な言葉で説明してみます。

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

この説明には、「vector<ScoredDocument>」の記述はどこにもありません。いちばん大切なのはスコアの配列です。テストコードは以下のようになると理想的です。

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

テストの本質を表現し、かつコードが1行になりました。これは、(手段は関数ですが、)独自のミニ言語のようなものです。

「CheckScoresBeforeAfter()」には、スコアの並びを表すカンマ区切りの文字列の引数が2つあります。カンマ区切りの数値をパースする関数を書くのは難しいことではありません。

CheckScoresBeforeAfter()の実装は、以下のようになります。

void CheckScoresBeforeAfter(string input, string expected_output) {
	vector<ScoredDocument> docs = ScoredDocsFromString(input);
	SortAndFilterDocs(&docs);
	string output = ScoredDocsToString(docs);
	assert(output == expected_output);
}

vector<ScoredDocument> ScoredDocsFromString(string scores) {
	vector<ScoredDocument> docs;

	replace(scores.begin(), scores.end(), ',', ' ');

	// 空白区切りの文字列から'docs' を作る。
	istringstream stream(scores);
	double score;
	while (stream >> score) {
		AddScoredDoc(docs, score);
	}

	return docs;
}

string ScoredDocsToString(vector<ScoredDocument> docs) {
	ostringstream stream;
	for (int i = 0; i < docs.size(); i++) {
		if (i > 0) stream << ", ";
		stream << docs[i].score;
	}
	return stream.str();
}

コードが多いように見えますが、「CheckScoresBeforeAfter()」を呼び出すだけでテストが書けるようになるので、これは強力です。これならもっとたくさんテストを書きたくなります。