見出し画像

TotT: テストを簡潔にするためのメソッド抽出

こんにちは、kubopです。
Googleにはトイレテスト(TotT)という文化があるようで、テストに関するTipsをトイレに貼り出し、テストに関する知識を全社で共有しているらしいです。

昨今はリモート勤務が広まり、TotTの実施は難しく、SlackやBotを用いてもなかなか浸透するかどうか…
そこで、noteに書きつつ自分が勉強するために、少しずつ読んで内容や、所感を書いてみようと思います。

※ 翻訳・解釈の間違いなどあるかもしれません。
その場合はこっそり教えてください。

もっとテストを書いてほしいのです。そう、あなたです。テストは、コードをリファクタリングするときや他の開発者が機能を追加するときに、 あなたを守るセーフティネットであることはもうおわかりでしょう。また、テストがコードの設計に役立つこともご存知でしょう。

私たちは、この秘密兵器(this secret weapon)を世界中の人々と共有し、私たちのテストへの情熱を広めるとともに、あなた自身やあなたの会社の他の人たちに、この重要なトリックやテクニックを楽しく簡単に学んでもらうことにしました。
このブログで定期的にエピソードを紹介し、PDFを提供しますので、プリントアウトしてご自分のバスルーム、廊下、キッチン、月面基地、秘密の地下要塞、億万長者の創業者のプリウスなど、どこにでも貼り付けてください。

Introducing "Testing on the Toilet"

TotT: Extracting Methods to Simplify Testing

既存のメソッドが長くて複雑な場合は、テストをするのが難しくなります。
メソッドを抽出することで、メソッドを簡潔にしてみましょう。
既存の複雑なメソッドの中から、メソッド呼び出しに置き換えることができるコードを見つけます。

このようなプロダクトコードはDBだけではなく、キャッシュにも依存しているため、テストが難しく、さらに取得した結果に対して後処理を行います。

def GetTestResults(self):
  # 結果がキャッシュされているかを確認する。
  results = cache.get('test_results'None)
if results is None:
                # キャッシュがない場合は、DBへ確認する。
                results = db.FetchResults(SQL_SELECT_TEST_RESULTS)

  # テストのpassingと、failingの数をカウントする。
  num_passing = len([r for r in results if r['outcome'] == 'pass'])
  num_failing = len(results) - num_passing
  return num_passing, num_failing

このメソッドは豊富にコメントがかかれているため、このメソッドの一部をコメントを参照してメソッドに切り出します。
そうすることで、コメント自体が不要になることがあります。

 def GetTestResults(self):
   results = self._GetTestResultsFromCache()
   if results is None:
     results = self._GetTestResultsFromDatabase()
   return self._CountPassFail(results)

# 結果がキャッシュされているかを確認する。
 def _GetTestResultsFromCache(self):
   return cache.get('test_results', None)

# キャッシュがない場合は、DBへ確認する。
 def _GetTestResultsFromDatabase(self):
   return db.FetchResults(SQL_SELECT_TEST_RESULTS)

# テストのpassingと、failingの数をカウントする。
 def _CountPassFail(self, results):
   num_passing = len([r for r in results if r['outcome'] == 'pass'])
   num_failing = len(results) - num_passing
   return num_passing, num_failing

このように抽出したメソッドは、元のメソッドよりもより個々の部分にフォーカスできるようになりました。
これには、コードの可読性をあげ、保守が用意になるという利点があります。

🤔

Rubyにすると以下のような感じでしょうか。

def get_test_results
  # 結果がキャッシュされているかを確認する。
  results = cache.get('test_results', None)
  if results.nil?
    # キャッシュがない場合は、DBへ確認する。
    results = db.FetchResults(SQL_SELECT_TEST_RESULTS)
  end
  # テストのpassingと、failingの数をカウントする。
  num_passing = results.select { |r| r['outcome'] == 'pass' }.count
  num_failing = results.count - num_passing

  { num_passing: num_passing, num_failing: num_failing }
end

上記のget_test_resultsである場合はテストケースが少なくとも2種類と、pass/failのカウント・境界値分とか諸々必要そうですね。

  • DBから結果を取得できた場合

    • passしたものがN個

    • failしたものがN個

  • キャッシュから結果を取得できた場合

    • passしたものがN個

    • failしたものがN個

describe get_test_results do
  context 'キャッシュから取得した場合' do
    it 'passが1件' {}
    it 'failが1件' {}
    ...
  end
  
  context 'DBから取得した場合' do
    it 'passが1件' {}
    it 'failが1件' {}
    ...
  end
end

RSpecを書くとこんな感じかな・・・?
より詳しく、正常・異常わけたり、passとfailの組み合わせなどを考慮しなければならなくなった場合は、1つのdescribe節の中が複雑になりそう。

以下は改善例

def get_test_results
  results = get_result_from_cache
  results = get_result_from_database if results.empty?
    return count_pass_fail(results)
end

def get_result_from_cache
  cache.get('test_results', None)
end

def get_result_from_database
  db.FetchResults(SQL_SELECT_TEST_RESULTS)
end

def count_pass_fail(results)
  num_passing = results.select { |r| r['outcome'] == 'pass' }.count
  num_failing = results.count - num_passing

  { num_passing: num_passing, num_failing: num_failing }
end

メソッドを分割してあると、細かくロジックをテスト出来るし、呼び出し元はインタラクションテストをすれば良さそう。
内部の仕組みを変えても、対象のメソッドしか失敗しない。そもそもprivateにしちゃえばいいな…。

describe get_test_results do
  # インタラクションテスト(メソッド呼び出し確認)でOK?
end

describe get_result_from_cache do
  ...
end

describe get_result_from_cache do
  ...
end

describe count_pass_fail do
  ...
end

Licensed under a Creative Commons
Attribution–ShareAlike 4.0 License

この記事が気に入ったらサポートをしてみませんか?