はじめに
こちらは著書「リーダブルコード」の学習記録です。
忙しい人のためにも、重要そうな部分を抜粋してまとめました。
14章 テストと読みやすさ
テストを読みやすくて保守しやすいものにする
目的は、他のプログラマが安心してテストの追加や変更をしやすくするためだ。
テストコードが大きくて複雑なものだったら、以下のようなことが起きる。
-
本物のコードを修正するのを恐れる
- 「うわぁ、このコードには手を出したくない...」
-
新しいコードを書いたときにテストを追加しなくなる
- テストのあるモジュールが減っていく。そして正しくコードが動作しているのか自身が持てなくなる
テストを読みやすくする
一般的な設計原則として、「大切ではない詳細はユーザーから隠し、大切な詳細は目立つようにする」べきだという。
以下は、検索結果のスコアをソートしてフィルタする関数のコード。
// 'docs'をスコアでソートする(降順)。マイナスのスコアは削除する。
void SortAndFilterDocs(vector<ScoredDocument>* docs);
テストは以下のようになっている。
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);
}
このテストのどこがダメなのか?
- テストステートメント(一連の命令や条件文)が長い。言い換えると、どうでもいいことがたくさん書かれている
- テストが簡単に追加できない。思わずコピペしてしまいそうになり、長くて重複の多いコードになる
- 一度に全てのことをテストしようとしている。ここではマイナスのフィルタリングとソート機能のテストだ。テストは分割した方が読みやすい
※投稿の都合上、問題点を抜粋しています(本書では全部で8個あります)
これを以下のようなヘルパー関数を使って簡潔にする
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]である
- スコアはこの順番でなければいけない
この説明には、vector<ScoredDocument>
の記述がどこにもない。一番大切なのはスコアの配列なので、以下のように修正する。
CheckScoresBeforeAfter("-5, 1, 4, -99998.7, 3", "4, 3, 1");
これで1行にまとめられる。
1つの機能に複数のテスト
コードを検証する「完璧」な入力値を1つ作るのではなく、小さなテストを複数作る方が、簡単かつ効率的で読みやすい。
その目的は複数のテストで別々の方向からバグを見つけ出すためだ。
CheckScoresBeforeAfter("2, 1, 3", "3, 2, 1"); //ソート
CheckScoresBeforeAfter("0, -0.1, -10", "0"); //マイナスは削除
CheckScoresBeforeAfter("1, -2, 1, -2", "1, 1"); //重複は許可
CheckScoresBeforeAfter("", ""); //空の入力は許可
このようにテストケースが分割されていれば、次の人がコードを扱いやすくなる。意図せずにバグを発生させていたとしても、失敗したテストによってその場所がわかる。
やりすぎ
場合によっては、テストに集中しすぎてしまう可能性もある。
-
テストのために本物のコードの読みやすさを犠牲にしてしまう
本物のコードは単純で疎結合*なものにする。テストは読み書きしやすくする。 -
テストのカバレッジを100%にしないと気が済まない
コードの10%にはユーザーインタフェースや重要ではないエラーケースが含まれている。その部分はバグのコストが高くないので、テストが割に合わない。
*疎結合:システムの各構成要素(モジュール、クラスなど)が独立して動作できるような状態のこと。
まとめ
- テストは読みやすくて保守しやすいものにする
- 最小のテストを作る
- テストケースは機能ごとに分割する
おわりに
要約はこちらの記事で以上となります。
私もそうですが、本を読んで色々わかった気になりがちな時があります。ですが、読みやすいコードを実際に書いてみることが当たり前だけどとても大事なことだと著者も言っています。
「業務で忙しくて読みやすいコードについて考える余裕がない」という方にも役立てられれば幸いです。