はじめに
こんにちは、新卒2年目エンジニアの細川です!第4回目となった社内輪読会!
前回までの記事も含めてこちらにありますのでぜひ確認してみてください!
引き続き「リーダブルコード」を読み進めていきます。今回の担当は「14章 テストと読みやすさ」です。
14章は皆さん大好きなテストについて書かれています!読みやすくて良いテストを作ることで、
テストを追加するのが楽しくなり、結果的にカバレッジの低下を防ぐことにもつながるので、ぜひ皆さんも読みやすいテストをかけるようになりましょう!
読みにくいテストと読みやすいテスト
まず、以下に読みにくいテストと改善したコードを示します。
ScoredDocument
型の配列を渡すと、負の値のscore
を持つ要素を除外し、残った要素をscore
が大きい順に並べるSortAndFilterDocs()
という関数のテストコードです。
なお、元のソースのC言語?が慣れてなくて若干読みづらかったので、TypeScriptで書き換えています。そのため、ところどころ本と違うところがありますが、基本的なソース自体は大きく変更していないので、ご了承ください。
// 読みにくいテスト
type ScoredDocument = {
url?: string;
score?: number;
};
const Test1 = () => {
const docs: ScoredDocument[] = [{}, {}, {}, {}, {}];
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.equal(3, docs.length)
assert.equal(4, docs[0].score)
assert.equal(3.0, docs[1].score)
assert.equal(1, docs[2].score)
};
// 読みやすいテスト
type ScoredDocument = {
url: string;
score: number;
};
const Test1 = () => {
CheckScoresBeforeAfter([-5, 1, 4, -99998.7, 3], [4, 3, 1]);
};
//-----------以下テスト用の関数--------------------------------------------------
const AddScoredDoc = (docs: ScoredDocument[], score: number) => {
const url: string = "http://example.com";
const sd: ScoredDocument = { url: url, score: score };
docs.push(sd);
};
const CheckScoresBeforeAfter = (before: number[], after: number[]) => {
const docs: ScoredDocument[] = ScoredDocsFromNumber(before);
SortAndFilterDocs(docs);
const scores: number[] = ScoredDocsToNumber(docs);
assert.equal(after, scores);
};
const ScoredDocsFromNumber = (scores: number[]): ScoredDocument[] => {
const docs: ScoredDocument[] = [];
scores.forEach((score) => {
AddScoredDoc(docs, score);
});
return docs;
};
const ScoredDocsToNumber = (docs: ScoredDocument[]): number[] => {
const scores: number[] = [];
docs.forEach((doc) => {
scores.push(doc.score);
});
return scores;
};
二つを見比べると、違いはたくさんありますが、最も大きな点としてはTest1()
内の処理が1行になっていることです。
このように、テストは最終的に1行にまとめられることが多いです。以前学んだ「12章 コードに思いを込める」のように、このテストで確かめたいことを言葉でまとめてみると以下の通りになりなります。
- 元の文書のスコアは[-5, 1, 4, -99998.7, 3]である。
SortAndFilterDocs()
を呼び出した後のスコアは[4, 3, 1]である。- スコアはこの順番でなければならない。
これを満たすようにテストを書くとおのずと、
CheckScoresBeforeAfter([-5, 1, 4, -99998.7, 3], [4, 3, 1]
このような記述になるはずです。こうすることで、テストケースの追加などを行いやすくなります。また、それ以外にも元データを用意するために何度も”http://example.com”の文字列を書いていたりとテストに関係のない情報が目立ってしまっているので、これらも外に出しています。
読み手のために、「大切ではない詳細はユーザーから隠し、大切な詳細は目立つようにする」というのが大事です。
読みやすさ以外のポイント
テストコードの読みやすさ以外の、良いテストを書くためのポイントについても書かれていました。まとめると以下のものが挙げられます。
- エラーメッセージを読みやすく!
- 入力値を適切に!
- 名前は正確に
1つめのエラーメッセージについては、より具体的でわかりやすいメッセージが出るassert関数を利用しましょうということです。「どのテストの何行目で落ちました」みたいなエラーメッセージが出たほうが、分かりやすいですよね。既存のものがわかりにくかったら自作してもいいと書かれてました。大きいプロジェクトとかでは特に最初の方にこういう部分を作ってしまっておいたら楽になりそうです。
2つめの入力値については、先ほどの例のテストコードでも言えることですね。気づかれた方も多いと思いますが、テスト対象の関数がマイナスの値を除外するかどうか確かめるためにわざわざ-99998.7のような値を使う必要は無いですよね。-1とかが一つあればよさそうです。
また、逆に足りないものとして0はテストケースにあってもよさそうです。また、それらをすべて含めた完璧な入力値を一つ作るのではなく、パターンによって以下のようにテストケースを複数作った方がわかりやすくなります。
CheckScoresBeforeAfter([2, 1, 3], [3, 2, 1]); // ソート
CheckScoresBeforeAfter([0, -0.1, -10], [0]); // マイナスは削除
CheckScoresBeforeAfter([1, -2, 1, -2], [1, 1]); // 重複は許可
CheckScoresBeforeAfter([], []); // 空の入力は許可
3つめの名前はテスト関数の名前ですね。Test1()
などではなく、Test_関数名
などのようにわかりやすく名付けるべきです。そうすることで、assertなどで出るエラーメッセージもより分かりやすくなります!またテスト関数自体を他から呼び出すことは基本的にないはずなので長い名前になっても問題ありません。
テスト駆動開発(TDD)について
皆さんはテスト駆動開発(TDD)なるものを聞いたことがありますでしょうか?僕は今回初めて聞いた単語なのですが、簡単に言うと実装よりも前にテストを書いてしまい、テストが通るように実際のソースコードを書き、リファクタリングしていくというような開発手法のようです。
気になった方はテスト駆動開発とかでググってみてください。すでにこちらのようにまとめてくださっている方がたくさんいます。
実際のプロジェクトでこのような進め方を実践するのはなかなか難しいかもしれませんが、仕様などが細部までかっちりと決まっていて手戻りがない場合などは少し試してみてもいいのかもしれません。テストを先に書くとまではいかなくても、テストを想像しながら、テストしやすいコードを書くように意識するだけでも、いいコードが書けるようになると思いますので、まずはそこから始めてみようかと思います!
リーダブルコードでもテストに関するやりすぎには釘を刺してくれています。
元のソースコードの読みやすさを犠牲にしてまで、テストのきれいさを追求しようとしたり、カバレッジ100%を目指すなどはしなくていいと言ってくれているので、皆さんもやりすぎは気を付けましょう。
まとめ
テストを良くするためのまとめとしては以下の通りです
- テストのトップレベルは簡潔に!入出力テストは1行で書けたらいいね!
- エラーメッセージはわかりやすくね!
- 入力値は単純かつ適切に!
- テストの名前はちゃんと付けよう!
おわりに
今回はテストについてまとめられていました。
テストを簡潔に書くことでテストを追加するのが楽しくなるというのはすごくその通りだと思いました。テストの追加が面倒だとどうしても後回しになってしまいがちなので、テストのことを考えながら実装していこうと思いました。
また、最近の言語だとテストは並列実行するものも多いので、分かりやすいテストを書くことはもちろん、実行順序に左右されない(他のテストで状態が変わった前提で通るテストを書かないなど)を意識することもすごくすごく大事だと思います!
ソースコードに比べるとどうしてもテストは後回しになってしまうかもしれませんが、できるだけ意識的にテストをしっかり書くように意識してみましょう!