「リーダブルコード」を改めて読んでみて(その7)

「リーダブルコード」に関する記事の続きです。今回が最終回になります。長かった…笑

今回の記事では第Ⅳ部の中の14章の内容に触れていきます。
15章に関しては、書籍を読んでいただく方が理解を深めやすいと感じたので割愛します。

第Ⅳ部は「選抜テーマ」というテーマで、14章では以下の点にフォーカスしていました。


どこからともなく「テスト書いてますか?」と聞こえてきそうですが笑、僕自身テストコードをほとんど書いたことがないので、戒めの意味も込めてじっくり読みました。


テストと読みやすさ

14章では、テストと読みやすさというタイトルで、読みやすいテストコードの書き方について言及しています。

ここでいう"テスト"は、本物の(=実際にシステム側で使用される)コードの振る舞いを確認するためのすべてのコードとして定義されています。

重要だと感じたのは以下のフレーズです。

  • テストコードを読みやすくすることは、テスト以外のコードを読みやすくするのと同じくらい大切なことだ。
  • テストコードというのは「本物のコードの動作と使い方を示した非公式的な文書」だと考えるプログラマもいるほどである。
  • テストコードが読みやすければ、本物のコードの動作が理解しやすくなる。


つまり、テストコードを書く際も、ここまでの章で学んできたことをそのまま当てはめてあげればいいということです。


ここからは、あるテストコードを読みやすいテストコードへとリファクタリングしていく過程を見ていきます。
サンプルコードがC++で書かれており、私自身が処理内容(特にポインタ渡しと参照渡しの違い)に関して正確な理解をできている自信がないので、間違いがあったり理解しやすいサイトなどありましたらコメントいただければ幸いです。

テスト対象の関数は、以下のSortAndFilterDocs()です。
ScoredDocumenturlscoreをフィールドに持つクラスです。
docsの要素をスコアの降順でソートし、スコアがマイナスの要素を削除する処理を行います。

void SortAndFilterDocs(vector<ScoredDocument>* docs);


また、SortAndFilterDocs()に対するテストコードは以下です。

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);
}


このテストコードをリファクタリングしていきます。

まず最初に、「無関係の下位問題を抽出する」作業を行います。
urlscoreをセットしている箇所はこのテストの目的と直接関係のない処理なので、これらを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;
  AddScoredDoc(docs, -5.0);
  AddScoredDoc(docs, 1);
  AddScoredDoc(docs, 4);
  AddScoredDoc(docs, -99998.7);
  AddScoredDoc(docs, -5.0);

  SortAndFilterDocs(&docs);

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


これで多少はすっきりしましたが、テストのための前処理と後処理がノイズとして残ってしまっています。
そこで、次は「コードに思いを込める」作業を行います。このテストで評価したいことは以下のようになります。

  • SortAndFilterDocs()実行前の文章のスコアは [-5, 1, 4, -99998.7, 3] である。
  • SortAndFilterDocs()実行後の文章のスコアは [4, 3, 1] である。スコアはこの順番でなければいけない。


上記に基づいて処理をCheckScoresBeforeAfter()として整理します。
これによって、「このような状況と入力からこのような振る舞いと出力を期待する」というレベルまで要約できます。

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();
}

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

void Test1() {
  CheckScoresBeforeAfter("-5, 1, 4, -99998.7, 3", "4, 3, 1");
}


最後に、テストコードに「"いい名前"を付ける」作業を行います。
基本的に以下のことが分かるように命名すると、読み手はテストの内容を理解しやすくなります。

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


今回はクラスがないので、テストコードの関数名をTest_テストする関数_テストする状況()に変更します。
この例の場合、入力にマイナスの値があるテストケースになります。

void Test_SortAndFilterDocs_NegativeValues() {
  CheckScoresBeforeAfter("-5, 1, 4, -99998.7, 3", "4, 3, 1");
}


他にもアサーションチェック(assert())が失敗したときの出力フォーマットやテストケースの値の選び方などが記載されていましたが、テストコードを読みやすくするにあたって個人的に重要と感じたことは説明できたので割愛します。

「リーダブルコード」に関する記事は以上となります。
何となくは理解していたことを言語化された状態で整理できたので、改めて読んでよかったです。

次はもう少し実装寄りというか、コードベースの記事を書いていけたらと思っています。


一連の記事はこちらです。
tkhs0604.hatenablog.com tkhs0604.hatenablog.com tkhs0604.hatenablog.com tkhs0604.hatenablog.com tkhs0604.hatenablog.com tkhs0604.hatenablog.com