give IT a try

プログラミング、リモートワーク、田舎暮らし、音楽、etc.

過度なDRYは読みやすさの敵!?「リーダブルテストコード」という発表をしました #vstat

先日、このブログでもお伝えしましたが、「VeriServe Test Automation Talk No.3」というオンラインイベントで登壇してきました。

veriserve-event.connpass.com

申込者数はなんと1000人を超えていて、大変驚きました。

僕は「リーダブルテストコード」というテーマで発表しました。スライドはこちらです。

Twitterでたくさんシェアされたり、はてなブックマークがたくさん付いたり、こちらもすごい反響でビックリしました。

で、どんな内容だったの?

ひとことで言うなら「テストコードを徹底的にDRYにしようとしちゃダメよ!」というお話です。
このネタは昔からQiitaやTwitterとかでことあるごとに話してきましたが、この勉強会であらためてなぜダメなのか、DRYに書かず、どう書くべきなのか、という話を力説してみました。

優秀なプログラマほど、「DRYが善」と信じて止まないので、テストコードに対してもDRYを追求しようとしてしまいがちです。
今までいろんな人のテストコードをレビューしてきましたが、僕が初めてコードレビューした人の9割ぐらいがテストコードをDRYに書こうとしていたように思います。
まあ、「コードを見たらDRYにしたくなる」というのは、ある意味プログラマの職業病みたいなものなので仕方ない面もあるのですが。

とはいえ、それだといつまでたっても読みやすいテストコードは書けません。
読みやすいテストコードを書くためには、「あえてDRYを捨てる」という発想の転換が必要です。

こういう考え方が世の中のプログラマにもっと広がってほしいな〜と思って、今回のような発表内容に至りました。

他の登壇者のみなさんのスライド

僕の他にも末村 拓也さんと風間 裕也さんが、同じく「リーダブルなテストコード」について、非常にためになる発表をされていました。
こちらもぜひご覧ください。


質疑応答&パネルトークで話した内容

勉強会の後半は登壇者3人とモデレータによる質疑応答&パネルトークでした。
いろんなQ&Aのネタがあったのですが、その中から1つだけ僕が話したトピックをここに書いておきます。

Q. アプリケーション側に定数があった場合、その定数をテストコード側でそのまま利用しても良いか?

定数にもいろんな定数がありますが、ここでは一例として消費税の税率を定数化していた場合を考えてみます。

たとえばこんな感じですね。

class Product
  TAX_RATE = 0.1
  # ...
end

テストコードは以下のようにProductクラスで定義した定数を参照しています。

example '税込み価格が返る' do
  product.price = 1000
  expect(product.price_including_tax).to eq \
    product.price * (1 + Product::TAX_RATE)
end

このテストコードを評価する場合、良い・悪いでどちらかひとつ選べと言われたら、僕は「悪い」を選択します。
僕だったらテストコードはこんなふうに税込み価格をベタ書きします。

example '税込み価格が返る' do
  product.price = 1000
  expect(product.price_including_tax).to eq 1100
end
「なぜ悪いの?」

テストコード内で定数Product::TAX_RATEを参照した場合、将来もし消費税率が15%に変わっても定数の値を変えるだけで修正が終わります。テストコードには何も手を加える必要がありません。

一見、これはDRYであることのメリットのように思えますが、実はデメリットになる可能性も大いにあります。

アプリケーション側の仕様変更に合わせてテストコードが仲良く二人三脚で歩いて行くようなコードを書くと、アプリケーション側の実装にバグが埋め込まれた場合にもテストがパスしてしまう可能性があります。つまり、バグを検知すべきテストコードがやすやすとバグを見逃す可能性があるわけです。これは致命的な問題です!

class Product
  # 寝ぼけたプログラマが消費税率15%をそのまま15としてしまった!
  TAX_RATE = 15
  # ...
end
example '税込み価格が返る' do
  product.price = 1000
  # 税込み価格が1万6000円になってるのにテストがパスしてしまう!!
  expect(product.price_including_tax).to eq \
    product.price * (1 + Product::TAX_RATE)
end
ベタ書きした方がバグを検知しやすい

一方、expect(product.price_including_tax).to eq 1100のようにベタ書きしてあれば、定数を変更したときにテストが失敗します。
そして、次のようなテストコードに書き替えれば、TAX_RATE = 15という設定値が間違いであることにも気付くことができます。

example '税込み価格が返る' do
  product.price = 1000
  # TAX_RATE = 0.15 でなければこのテストは失敗する
  expect(product.price_including_tax).to eq 1150
end

こうした理由から、テストコード内でアプリケーション側の定数を参照するのは避けた方が良い、と考えることができます。

「でも、DRYにしないとテストコードの修正が大変なんです!」

もちろん、テストコードはDRYではなくなるため、仕様変更が発生すると大量のテストが失敗することもあります。ただ、それはデメリットではなく、仕様変更のインパクトがテストコードのおかげで可視化された、と好意的に捉えるようにしましょう。

ただし、「これはある程度DRYにしておかないと、今後のテストコードの保守がかなりキツい」という場合は「ほどほどのDRY」は許容しても構いません。
そういうケースでは僕はrspec-parameterizedというgemを使って、パラメタライズドテストを書くことが多いです。

github.com

Twitter上のみなさんの反応など

ツイートを見る限り、おおむね好評だったようでほっとしました😄

なお、当日の参加者のみなさんのツイートはこちらにまとめられています。
togetter.com

おまけ

いつものことなのですが、登壇前には何度もリハーサルして時間配分やトーク内容を洗練させていっております。

僕が登壇前にどんな準備をしているのか知りたい方は、こちらのエントリをご覧ください。
blog.jnito.com

まとめ

というわけで、このエントリでは先日登壇した「VeriServe Test Automation Talk No.3」というオンラインイベントの登壇内容を紹介してみました。
もっと読みやすいテストコードを書きたい!という人はぜひ今回の登壇内容を参考にしてみてください。

最後に、このイベントに参加してくださったみなさん、登壇者のみなさん、そして運営者のみなさん、どうもありがとうございました!
久々のオンライン登壇でしたが、みなさんのおかげで楽しく発表することができました😄

あわせて読みたい

勉強会当日にいただいた質問に一通り答えてみました。こちらもあわせてどうぞ!

blog.jnito.com

PR:RSpecの本とか、Rubyの本とか

この講演を聞いてテストコードに俄然興味が沸いてきた!という人は、ぜひ「Everyday Rails - RSpecによるRailsテスト入門」をどうぞ。
RSpecを使った実践的なテストコードの書き方が学べます。
leanpub.com

また、「プロを目指す人のためのRuby入門」でも例題を解く際にテスト駆動開発のスタイルを取り入れてます(使用しているテスティングフレームワークはminitestです)。
Rubyと同時にTDDを学びたい方はこちらもぜひ!