【はじめての輪読会】リーダブルコード14章

◆ Live配信スケジュール ◆
サイオステクノロジーでは、Microsoft MVPの武井による「わかりみの深いシリーズ」など、定期的なLive配信を行っています。
⇒ 詳細スケジュールはこちらから
⇒ 見逃してしまった方はYoutubeチャンネルをご覧ください
【5/21開催】Azure OpenAI ServiceによるRAG実装ガイドを公開しました
生成AIを活用したユースケースで最も一番熱いと言われているRAGの実装ガイドを公開しました。そのガイドの紹介をおこなうイベントです!!
https://tech-lab.connpass.com/event/315703/

はじめに

こんにちは、新卒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章 コードに思いを込める」のように、このテストで確かめたいことを言葉でまとめてみると以下の通りになりなります。

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

これを満たすようにテストを書くとおのずと、

CheckScoresBeforeAfter([-5, 1, 4, -99998.7, 3], [4, 3, 1]

このような記述になるはずです。こうすることで、テストケースの追加などを行いやすくなります。また、それ以外にも元データを用意するために何度も”http://example.com”の文字列を書いていたりとテストに関係のない情報が目立ってしまっているので、これらも外に出しています。

読み手のために、「大切ではない詳細はユーザーから隠し、大切な詳細は目立つようにする」というのが大事です。

読みやすさ以外のポイント

テストコードの読みやすさ以外の、良いテストを書くためのポイントについても書かれていました。まとめると以下のものが挙げられます。

  1. エラーメッセージを読みやすく!
  2. 入力値を適切に!
  3. 名前は正確に

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行で書けたらいいね!
  • エラーメッセージはわかりやすくね!
  • 入力値は単純かつ適切に!
  • テストの名前はちゃんと付けよう!

おわりに

今回はテストについてまとめられていました。

テストを簡潔に書くことでテストを追加するのが楽しくなるというのはすごくその通りだと思いました。テストの追加が面倒だとどうしても後回しになってしまいがちなので、テストのことを考えながら実装していこうと思いました。

また、最近の言語だとテストは並列実行するものも多いので、分かりやすいテストを書くことはもちろん、実行順序に左右されない(他のテストで状態が変わった前提で通るテストを書かないなど)を意識することもすごくすごく大事だと思います!

ソースコードに比べるとどうしてもテストは後回しになってしまうかもしれませんが、できるだけ意識的にテストをしっかり書くように意識してみましょう!

 

アバター画像
About 細川 13 Articles
高専専攻科制御系の学科を卒業後、2022年にサイオステクノロジーに入社。入社後は、バックエンド, フロントエンドの開発に携わり、Go言語やTypeScriptを用いた開発を行っている。その他、React, Next.JS, Nest.JS, GraphQLなど
ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

0人がこの投稿は役に立ったと言っています。


ご覧いただきありがとうございます。
ブログの最新情報はSNSでも発信しております。
ぜひTwitterのフォロー&Facebookページにいいねをお願い致します!



>> 雑誌等の執筆依頼を受付しております。
   ご希望の方はお気軽にお問い合わせください!

Be the first to comment

Leave a Reply

Your email address will not be published.


*


質問はこちら 閉じる