TotT: テストを簡潔にするためのメソッド抽出
こんにちは、kubopです。
Googleにはトイレテスト(TotT)という文化があるようで、テストに関するTipsをトイレに貼り出し、テストに関する知識を全社で共有しているらしいです。
昨今はリモート勤務が広まり、TotTの実施は難しく、SlackやBotを用いてもなかなか浸透するかどうか…
そこで、noteに書きつつ自分が勉強するために、少しずつ読んで内容や、所感を書いてみようと思います。
※ 翻訳・解釈の間違いなどあるかもしれません。
その場合はこっそり教えてください。
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