最近、会社の後輩や、業務としてRails未経験の友人から、テストの書き方について質問されることがありました。プロダクトコードは実際に実現したいことがあるのでそれを実現するために実装すれば良いため、イメージしやすいのですが、テストコードは最初はイメージしにくいのでしょう。
そこで、これまでの経験を踏まえ、テストを書き始めるための基本的に知っておくべきことについて記述したいと思います。
以下では、テストのフレームワークとしてRSpecを利用する前提で書きます。
また、モデルを生成には、FactoryBotというライブラリを利用します。FactoryBotはRailsではモデル生成で広く利用されているライブラリです。FactoryBotの導入は他の記事に任せます。
テストを書きつづけるための準備
これから、Railsにおけるテストについてのことはじめを述べていきますが、その前に、テストを書き続けるための準備をしておきましょう。テストは書いて終わりではありません。ソースコードを書いてクラウド上(GitHubやGitLab、Bitbucketなど)にpushしたら、自動的にこれまで書いたテストが回る様にしてそれをエンジニアが気づく仕組みを作る必要があります。そのためのツールを紹介します。
まずは、継続的インテグレーション(CI)サービスです。有名なのはCircleCI、TravisCIなどです。自前で立ち上げる場合はJenkinsなどのオープンソースもあります。チームメンバーと相談して、どのサービスを利用するかを確認しましょう。ちなみに私は自前で立ち上げる手間を省くため、CircleCIやTravisCIを利用することが多いです。1
継続的にテストを回す仕組みができたのであれば、テストのカバレッジも継続的に測定してあげる必要があります。Railsであれば、simplecovというgemを利用することが多いでしょう。そして、カバレッジを可視化するためにCodeCovやCode Climateといったサービスを利用するとよいでしょう。この時、CircleCIを使っている場合はconfigファイルの追記などが必要になりますが、その辺りは公式のドキュメントやその他の解説サイトに委ねます。
テストを書く範囲
全てのコードに対してテストを書くことができればそれに越したことはありません。しかし、それは事実上不可能です。テストカバレッジが100%とすることのコストに見合うほどのメリットはないと言って良いでしょう。そこで、どういうところからテストを書き始めれば良いのかということについて考えてみます。
ビューがある場合
ビューがある場合のRailsのアーキテクチャを大まかにレイヤーに分けると以下のようになります。
ビュー |
コントローラー |
モデル |
基本的な考え方は、この中で、一番下のレイヤー(モデル)と一番上のレイヤー(ビュー)を中心にテストを書くということとです。
その中でも、真っ先に書かなければいけないのがモデルに対するテストです。ここではモデルという言葉はActiveRecordないし、プレーンなRubyオブジェクト等とします。Railsにおけるモデルには、業務ロジックが記載されます。例えば、請求書を作る時にBillというモデルがあって、そこでは、注文履歴から、請求明細を作るという業務ロジックが書かれます。このロジックは、実際の業務のソースコードの焼き写しですから、複雑になりがちです。また、この場合、ユーザーに対して請求を行うわけですから、絶対に間違ってはいけません。コードの中でも重要度は高い部分と言えるでしょう。こういうところは絶対にテストを十分に書くべきです。経験上、テストを十分に書いたと思ったとしても、実際には考慮漏れはあることでしょう。そしてその修正をしたと思ったら既存の挙動が壊れてしまう。テストがあれば壊れたことに気づけますが、テストがなければ気づくことができません。
一方で、一気通貫的な振る舞いをテストも書くべきです。例えば何かを購入するサイトであれば、商品を検索し、商品を選んでもらい、カートに保存、決済に進み、完了するまでの一連の流れです。Railsでは、SystemSpec(以前はFeatureSpec)に該当するものです。この一連の流れをきちんと書いておくことができれば、コントローラーの実装も担保できますし、モデルのロジックも全部ではないとはいえ、担保できます。ただ、SystemSpecでありとあらゆるパターンを網羅するのは必ずしも得策ではない場合があります。SystemSpecはモデルのテストよりも実行時間がかかるのが一般的ですし、細かなパターンの違いはモデル側のテストで吸収するのも一つの方法です。一方で、SystemTestのいいところはホワイトボックス的にテストをかけることです。内部実装を知らない人でも、業務に精通していればシナリオを洗い出せます。テストにかける余裕がある場合は業務に詳しいQAエンジニアにSystemSpecを書いてもらって、実装者に実装漏れを気づかせることもできます。
APIを提供する場合
APIの場合は、ビューが無くなりますが、代わりにJSONやXMLなどの形式で値を返すことになるでしょう。
JSON等 |
コントローラー |
モデル |
この場合、SystemSpecは書けませんので、RequestSpecをかくことになります。しかし、考え方は同じで、ロジックが書かれているモデルのテストはきちんと書き、APIのインターフェースもきちんとテストを書くということです。
まとめますと、業務ロジックがコテコテに書かれているモデルに関しては面倒臭がらずにきちんとテストを書き、大きな挙動・流れの確認はSystemSpec(APIの場合はRequestSpec)で書くことで、下のレイヤーと上のレイヤーの挙動を担保し、全体としての挙動を担保すると考えれば良いでしょう。
逆に、ControllerやControllerのconcernのテストを書かなければと思った場合は、設計が間違っていると思ったほうがいいでしょう。controllerに業務ロジックは書く必要はないし、そのような場合は、既存のモデルが持つべきロジックであればそちらに実装すべきですし、そうでなければプレーンなRubyのクラスとして実装することも検討したほうが良いでしょう。このようにして、テストを書くという観点からシステム全体を見直すと、設計が良くないということに気づくことができます。これもテストを書くことの一つの利点です。
モデルのテスト
それでは、モデルのテストについてはどういう観点でテストを書けば良いのでしょうか。
私は、モデルのテストの観点は大きく分けて2つあると考えています。一つめはバリデーションのテスト、二つ目はモデルの外で使用される可能性のあるメソッド(publicメソッド)のテストです。モデルの外で利用される可能性があれば、インスタンスメソッドであれ、クラスメソッドであれ、テストを書くべきでしょう。ActiveRecordのscope
もクラスメソッドに含まれるので、テストを書きます。
以下の例では、次のようなActiveRecordのモデルで説明をします。
バリデーションのテスト
例えば、次の様なバリデーションがあったとしましょう。OrderItemモデルには、family_name
(苗字)とgiven_name
(名前)の属性を持っており、その両方を合わせたもの(フルネーム)の長さの制限を20文字とするバリデーションです。
机上で構いませんので、モデルのテストを書いてみましょう。
class OrderItem < ApplicationRecord
validates :full_name, length: { maximum: 20, message: :too_long_sum }
def full_name
"#{family_name}#{given_name}"
end
end
リスト1. OrderItemにfull_nameのバリデーションを追加
今回は、full_nameのバリデーションのテストを以下の様に実装してみました。
RSpec.describe OrderItem, type: :model do
describe '#valid?' do
let(:order_item) { FactoryBot.build(:order_item, attributes) }
let(:attributes) { {} }
subject { order_item }
describe 'full_name' do
context '20文字の場合' do
let(:attributes) {
{ family_name: 'あ' * 10, given_name: 'い' * 10 }
}
it { is_expected.to be_valid }
end
context '21文字の場合' do
let(:attributes) {
{ family_name: 'あ' * 10, given_name: 'い' * 11 }
}
it {
is_expected.to be_invalid
# expect(subject.errors.keys).to contain_exactly(:full_name) # DEPRECATED. Rails 6.2ではattribute_namesを使う
expect(subject.errors.attribute_names).to contain_exactly(:full_name)
expect(subject.errors.full_messages).to contain_exactly(
'お名前は合計20文字以内で入力してください。'
)
}
end
end
end
end
リスト2. OrderItemにfull_nameのバリデーションのテストの実装例
バリデーションのテストの時のポイントは、テストしたいバリデーションにフォーカスをあてるということです。モデルのデフォルト値はFactoryBot
の定義されますが、このとき基本的にはvalidな値で定義しておきます。その上で、FactoryBot.build
する際にテストしたい属性のみ値を変更させ、バリデーションを実行させます。リスト2では、family_name
とgiven_name
の値を変更しています。
次に、validな場合のテストですが、こちらは簡単でvalidな値をattributes
に定義してあげ、it
ブロックのかでbe_valid
を呼んであげるだけです。RSpecのsubject
を利用するかどうかは流派によりますが、次の様にしても良いでしょう。
it { expect(order_item).to be_valid }
リスト3. subject
を利用しない場合の実装例
そして、ポイントは、invalidな場合のテストです。invalidであることをテストしたいのでbe_invalid
を確認するのは問題ないでしょう。大事なのは、ここで終わらせてはいけないということです。まず、注目している属性のみにエラーが入っていることを確認する必要があります。今回の場合、一つの属性だけのテストなので、他の属性のエラーが入ることはありませんが、複雑なバリデーションを実装すると、ある属性がinvalidになると、他の属性もinvalidになることがあります。例えば、full_name
の最大文字数は20字だが、family_name
単体での文字数は15字である場合を考えます。この時、family_name
がinvalidな場合は、まずはfamily_name
のエラーをユーザーに解消してもらいたいためにfull_name
のvalidationは行わないという様にしたいとします。実装はリスト4の様にします。
validates :family_name length: { maximum: 15 }
validates :full_name, length: { maximum: 20, message: :too_long_sum }
リスト4. family_name
にバリデーションを追加
この時、family_name
がinvalidな場合はfull_name
のバリデーションエラーは出したくないので、テストはリスト5のようになります。
context '苗字が16文字の場合' do
let(:attributes) {
{ family_name: 'あ' * 16, given_name: 'い' * 10 }
}
it {
is_expected.to be_invalid
# expect(subject.errors.keys).to contain_exactly(:family_name) # DEPRECATED. Rails 6.2ではattribute_namesを使う
expect(subject.errors.attribute_names).to contain_exactly(:family_name)
expect(subject.errors.full_messages).to contain_exactly(
'苗字は15文字以内で入力してください。'
)
}
end
リスト5. family_name
にバリデーションを追加した時のテストの例
しかし、リスト4の実装では、full_name
のバリデーションも実行されてしまい、このテストはfailします。この様に、ある属性のバリデーションを追加したことで、他のバリデーションにも引っかかってしまい、期待しない挙動となってしまうことがあります。そのために、expect(subject.errors.attribute_names).to contain_exactly(:family_name)
として、errors.attribute_names
が:family_name
のみであることを明確にしています。ここで expect(subject.errors.attribute_names).to include(:family_name)
としている方を見かけますが、これでは、errors.attribute_names
の配列に:family_name
が含まれていることしかテストできておらず、:family_name
以外が存在しないということができていません。contain_exacly
を使うことで他の属性にエラーがないということを担保できます。今回は、errors.attribute_names
の要素の順序は気にしなくても良いため、contain_exactly
を使いましたが、順序も重要な場合はmatch
マッチャ等を使い、順序性も担保してあげましょう。
ちなみに、family_name
がinvalidな場合にfull_name
のバリデーションを実行させないためには、例えば以下の様な実装になります。ちょっと汚いので、もう少し良い実装方法があるかもしれません。
validates :family_name, length: { maximum: 15 }
validates :full_name, length: { maximum: 20, message: :too_long_sum },
if: -> { !errors.include?(:family_name) }
リスト6. ifを利用して、family_nameがvalidな場合のみfull_nameのバリデーションを実行させる例
モデルのバリデーションの最後のテストは、エラーメッセージです。特に日本人向けのサービスの場合、i18nを利用してエラーメッセージを日本語化することがほとんどでしょう。モデルのバリデーションエラーののテストでは、エラーメッセージもテストするべきです。
前述のfamily_name
が15文字以下であることのバリデーションでは、length: { maximum: 15 }
としました。一方、full_name
が20文字以下であることのバリデーションでは、length: { maximum: 20, message: :too_long_sum }
のようにmessage
を追加しています。これは、family_name
とfull_name
それぞれでエラーメッセージを出し分けたいためです。family_name
は一つの属性であるため、苗字は15文字以内で入力してください。
というメッセージで良いのに対し、full_name
は二つの属性であるため、お名前は合計20文字以内で入力してください。
のように合計という言葉を追加したいわけです。
詳細な内部の仕様は追っていませんが、message
を追加しないとja.activerecord.errors.models.order_item.attributes.family_name.too_long
が呼ばれ、message
にtoo_long_sum
を追加すると、ja.activerecord.errors.models.order_item.attributes.full_name.too_long_sum
が呼ばれる様になります。
この辺りの挙動は、個人的にはやってみないとわからない様な気がしています。どのバリデーションエラー時にymlで指定したどのキーが使われるのかというのは、毎回調べるよりも実際に実行してみて確認する方が早いからです。そのためにもテストで確認しておくと実装が非常にスピードアップします。i18nが正しく設定されているかどうかを確認するという意味も込めて、エラーメッセージもきちんとチェックしておきましょう。そして、ここでも他の属性にエラーがないことを担保するためにcontain_exactly
を用いています。
以上が、モデルのバリデーションのテストについての簡単な説明です。今回は、最大桁数が20文字というところだけを注目して、full_name
が20文字および21文字の場合のみのテストを書きました。しかし、実際には、例えば、
・nil
のときはどうなのだろう。
・空文字のときは?
・最小値は1文字でよいのだろうか?
・family_name
が許容する文字は全てなのだろうか?髙
や𠮷
といったJISの第1水準、第2水準以外の文字も許容されるのだろうか?
などといった仕様が存在するはずです。もし仕様が明示的でない場合でも、テストを書くことで、仕様が明示的になっていないことに気づくことができます。テストはきちんと場合分けをして書く必要があるため、自然と頭の中が整理されるからです。1そのためにもテストを書くことは非常に大事なことなのです。
モデルの外で使用される可能性のあるメソッド(publicメソッド)のテスト
これまでの例で、インスタンスメソッドfull_name
のテストを書いてみましょう。ただし、単純にfamily_name
とgiven_name
を連結させただけではあまり面白みがないので、少しメソッドを拡張します。
def full_name(space: '')
"#{family_name}#{space}#{given_name}"
end
リスト7. full_nameメソッドに引数を追加し拡張
リスト7の様にspace
というキーワード引数を追加します。インスタンスメソッドfull_name
は変数space
の値でfamily_name
とgiven_name
が連結された値を解します。引数がなければデフォルト値が空文字となります。この場合のテストを書いてみましょう。開発者が書くモデルのテストは、ホワイトボックステストと呼ばれます。ホワイトボックステストとは、内部の論理構造を把握した上で、このメソッドの場合、if文はありませんが、例えば引数の有無でデフォルト値を使うかどうかが変わってくるので、その辺りを考慮すると、full_name
メソッドのテストは例えば以下の様になります。2
describe '#full_name' do
context '引数がない場合' do
subject { order_item.full_name }
it { is_expected.to eq 'てすと太郎' }
end
context '引数がある場合' do
subject { order_item.full_name(space: space) }
context 'spaceが全角スペースの場合' do
let(:space) { ' ' }
it { is_expected.to eq 'てすと 太郎' }
end
context 'spaceが半角スペースの場合' do
let(:space) { ' ' }
it { is_expected.to eq 'てすと 太郎' }
end
context 'spaceがnilの場合' do
let(:space) { nil }
it { is_expected.to eq 'てすと太郎' }
end
end
end
リスト8. full_nameメソッドのテスト
今回の例では、subject
にテストしたいメソッドをセットしています。
インスタンスメソッド以外にも、クラスメソッドなども同様にテストしていきます。3
SystemSpec
SystemSpecは、モデルのスペックと違って、ブラックボックス的な意味合いが強くなります。Capybaraを使ってブラウザ上での操作を模擬するため、モデルの関数名などの内部実装を知る必要はありません。したがって、ある一面では非常に簡単にテストを書くことができます。実際のブラウザのHTMLコードを確認しながらタグやname属性をみてspecを書いていけばいいからです。
ここで「ある一面では」と書いたのは、SystemSpecを書き始めるための下準備が非常に大変であるからです。ここの準備ができていれば、テストを書き始めることは非常に簡単ですが、ここの準備が大変なのです。画面を表示させるためには、多くの場合、事前にレコードを準備していく必要があります。そしてそのレコードは一つや二つですまない場合もあります。specを書くためにそのページを書く上での前提条件を把握していないといけないわけです。そのほかにも、テストしたい画面に行くまでが大変であることもあります。例えば、事前にログイン処理をしてページを数枚挟まないといけなかったり、検索機能がある場合は、ElasticSearchなどの外部サービスへAPIを叩く必要があったりします。テスト用のElasticSearchを準備するのか、はたまたモックで対応するのかなどといった追加の作業が発生します。しかし、ここさえクリアできれば、SystemSpecはレグレッションテストとして強力な武器になります。最初は準備するのは大変かもしれませんが、あとで幸せになるためにシステムが小さいうちに苦労をしておきましょう。
レコードの保存や更新まで確認するべきか
さて、SystemSpecではどこまでテストをすれば良いのでしょうか。最低限は、画面の挙動として、次のページに進めて正しく表示されることを確認します。このとき、ユーザーの一連の流れを考えてテストを書いていきます。つまりはシナリオテストです。SystemSpecの場合、モデルのspecではit
を使っていた箇所をscenario
と書くようになります。これは、SystemSpecがシナリオテストであるということの表れです。すなわち、ユーザーがどのような振る舞いをするかをテストするということです。そうして書かれたSystemSpecでは、大抵の場合、何かしらのレコードが保存されたり、更新されたりします。このレコードの保存や更新はSystemSpecにおいて確認するべき項目となるの
レコードの保存や更新まで確認すべきではないという見解の方もいます。SystemSpecでは、画面の振る舞いをテストするべきで、レコードが正しく保存されたかどうかはSystemSpecの範疇ではないという意見です。前述の様に私は、SystemSpecでもレコードの保存まで確認するべきだという立場です。理由としては、昨今のフロントエンドの挙動は年々複雑になっており、意図した値がサーバサイドに渡ってきているかどうかが分かりにくくなっているからです。
例えば、図1のように、個人名義と法人名義でフォームの形式が異なる場合を考えてみましょう。そして、OrderItem
には、個人名義を保存する属性としてfamily_name
、given_name
、family_name_kana
、given_name_kana
があり、法人名義を保存する属性としてcorporate_name
、corporate_name_kana
があるとします。
図1. 契約名義で個人名義と法人名義でフォームの形式が異なる場合
このフォームのHTMLをみてみると次のようになっています。
<%# 個人が選択された場合 %>
<input name="order_item[family_name]" placeholder="苗字">
<input name="order_item[given_name]" placeholder="名前">
<input name="order_item[family_name_kana]" placeholder="ミョウジ">
<input name="order_item[given_name_kana]" placeholder="ナマエ">
<%# 法人が選択された場合 %>
<input name="order_item[corporate_name]" placeholder="法人名">
<input name="order_item[corporate_name_kana]" placeholder="ホウジンメイ">
リスト9. 名義入力フォームのHTML
Railsでは、inputタグのname属性によって、フォームがモデルのどの属性に対応されるかを判別するので、name属性は非常に大事です。個人名義か法人名義かによって保存するモデルの属性もかわってくるので、name属性も変わってきます。この挙動の実装としては、例えば、個人が選択されたら、JavaScriptを用いてorder_item[family_name]
〜order_item[given_name_kana]
のフィールドを表示にして、order_item[corporate_name]
とorder_item[corporate_name_kana]
のフィールドが表示されている場合は、非表示にするという制御を行います。今回は、Vue.jsを用いてこの挙動を実装しました。具体的には、v-show
やv-if
を用いて表示の制御を行います。
では、図2のように、個人が選択された場合のフィールドに値が入力された時、サーバーサイドにはどのような値が送られてくるかわかりますでしょうか。
図2. 個人のフィールドに値を入力した場合
当然ですが、family_name, given_name, family_name_kana, given_name_kanaは送られてきます。ログで確認するとリスト10の様になります。
Parameters: {"order_item"=>{"family_name"=>"てすと", "given_name"=>"太郎", "family_name_kana"=>"テスト", "given_name_kan"=>"タロウ"}}
リスト10 図2の状態でサブミットしたときのParameters(一部省略)
では、corporate_name
やcorporate_name_kana
はどうでしょうか。サーバーサイドに値は渡ってくるでしょうか。
答えは、Vue.js側の実装によります。v-if
で実装されている場合は渡ってきませんし、v-show
で実装されている場合は値が渡ってきます。(未入力の場合は空文字(""
)で渡ってきます)したがって、v-show
で実装した場合、個人名義なのにcorporate_name
やcorporate_name_kana
が保存されてしまうことがあるわけです。この様な挙動のテストは画面上だけのテストでは担保することができません。これを担保するにはレコードの保存状態まで確認する必要があると私は考えています。
テストケースはどの様に洗い出せばいいか
モデルのテストの場合、ホワイトボックステストとして、実装の中身がわかっているという前提でテストを書けばいいので、言ってしまえばソースコードをみて分岐を確認し、C0網羅、C1網羅、C2網羅など4を考慮してテストを書いていけば良いです。では、SytemSpecの場合、どうすれば良いでしょうか。
ここでは、ユーザーは申し込み画面で必要事項を入力したあと、確認画面へ進み、確認画面後は、ユーザーの入力した値に応じて画面の遷移が変わる場合を考えてみましょう。そして、このお申し込みには、付帯サービスなるものが存在し、離島でない場合は付帯サービスの説明を確認画面で表示し、離島の時は表示しません。また、申込者が本人または配偶者の場合のみ付帯サービスの説明を表示しますが、本人・配偶者以外の場合は表示しないという制御が必要だとします。
また、付帯サービスの説明表示とは別に、申し込み画面ではポイントカード登録をするかどうかを聞く項目があり、ポイントカード登録をするを選んだ場合は、確認画面後にポイントカードを登録する画面へ遷移するものとします。これをまとめたものが表1になります。rspec対象と書かれたカラムは後ほど説明します。
さて、この12パターンのうち、どのくらいまでテストをする必要があるでしょうか。もちろん、12パターン全てテストをするに越したことはありません。しかし、今回は3項目でそれぞれ2つまたは3つの値しか取らないので、2 x 3 x 2 = 12パターンしかありませんが、項目数や取りうる値が増えた場合、この数は文字通り指数関数的に増えていきます。全てをテストすることはたとえ自動テストであっても事実上不可能です。そこで適度に間引いたテストケースを作る必要があります。その間引き方の考え方が組み合わせテストという考え方になります。組み合わせテストでは、2因子間網羅をすれば、妥当なテストケースが作成されていると見做すことが多いです。「因子」とは、前述した表1の例であれば、「契約場所」「申込者」「ポイントカード登録」の3つの項目を指します。2因子間網羅とは、3因子のうち2つの因子の組み合わせを網羅できているということです。具体的には、「契約場所」「申込者」の2因子では、「離島でない、本人」「離島でない、配偶者」「離島でない、本人・配偶者以外」「離島、本人」「離島、配偶者」「離島、本人・配偶者以外」の6つを網羅しているということです。
表1の場合、2因子間網羅となるように間引いてテストケースとして選んだのがrspec対象と書かれたカラムに◯をつけたパターンになります。この様にしてテストケースを作成していきます。組み合わせテストの技法については、組み合わせテストの用語「2因子間網羅」「直交表」「All-Pairs法」に詳しく書かれています。なぜ、2因子間網羅で妥当なテストといえるのかということについての言及もあります。
ふるまいのテストとテストコードの共通化
これまで述べてきた様に、SystemSpecではユーザーのリアルに近い挙動をテストすることができます。ユーザーは、例えば申し込み画面があれば最初からなんの迷いもなくスムーズに申し込むとは限りません。入力する値を間違えてしまったり、カナを入れる箇所に漢字を入れてしまったりということがあります。SystemSpecではこうした挙動も確認できるのが非常に強みです。そして、忘れがちなのが、実装していない挙動に対するテストです。たとえば、ブラウザバック。ブラウザの戻るボタンを押された時に挙動がおかしくならないか。あるいは、リロード。例えばリロードすると入力していた文字が消えてしまっていたり、postとgetで挙動が変わってきたりすることも確認が必要です。私が過去にみてきたソースコードでは、完了画面にてリロードをするとレコードが次々と挿入される実装になっていたり、同じ内容で何度もAPIコールされているといった実装がありました。5こうした挙動も忘れずに確認しておきましょう。リロードするとレコードが増えていないかというのも、レコードの保存まで確認しておけばテストすることが可能です。加えて、自分たちで実装しておきながらテストし忘れるのが、戻るボタンです。例えば入力項目を入力した後の確認画面で、一つ前のページに戻って修正できるようにした画面上のボタン。この挙動も忘れがちです。ここもpostでの実装なのか、getでの実装なのか、ケースによると思いますが、悩みどころの一つだと思います。申し込みフォームが複数ページにまたがる場合、前からの遷移の時は問題なく動いていても、戻ってきた場合は挙動がおかしいということもありえます。しっかりと確認しましょう。
そして、こうしたふるまいのテストを行う場合、全てのケースにおいてリロードだったり、ブラウザバックだったりのテストを書く必要は必ずしもありません。どこか1ケースでこうした挙動を紛れ込ませれば十分です。そこでしばしば問題となってくるのがテストコードが共通化されている場合です。例えばSharedExampleやメソッド化して切り出すなどしてテストコードが共通化されていると、このテストコードの場合だけこの挙動を差し込みたいと言ったことが難しくなります。無理やり行なった結果、SharedExampleやメソッドの引数を増やして対応し、共通化したコードの方は分岐がたくさんといったことになってしまいます。こうなると、テストコードの可読性が極端に低下します。テストコードの良さは、ユーザーの行動が上から読み下すことができるという点にあります。テストコードに複雑な分岐が入っていないということで、読み手は自信をもって実装の意図や使用を理解することができます。特にSystemSpecにおいてはテストコードの過度な共通化は避けたほうが無難です。逆に、共通化をしようと思うエンジニアは、前述の様なユーザーのふるまいに対して鈍感なエンジニアと言えるかもしれません。SystemSpecは現実の複雑な仕様を反映させたものです。共通化するということは実装者がその複雑な仕様を理解しているということです。そして、それを確実にあとから参画したエンジニアにも伝える自信が必要です。我々はそこまで仕様を理解しているのでしょうか。今後変化していく仕様に対して追従できる柔軟なテストコードになっているでしょうか。テストコードはプロダクションコードとは明確に役割が違います。テストコードは簡潔でわかりやすく書くことが最優先です。テストコードを過度に共通化して満足しているのは一人のエンジニアのエゴと言えるかもしれません。
自動テストは愛
ここまで、モデルのテスト、SystemSpecについて、Railsでテストを書くにあたっての考え方、注意点を記載してきました。最後に伝えたいこと。それは、テストを書くということは愛であるということです。プロダクションコードは実装したその瞬間には完璧に理解しているかもしれません。しかし、半年後、1年後の自分がそのコードの意味を理解できていると言えるでしょうか。半年後、1年後に修正しなければならくなったときに、求められている仕様を間違えずに修正できるでしょうか。私は無理です。しかし、テストコードを書き、自動テストが落ちることで、仕様を思い出すことができます。つまり、テストコードを書くということは、半年後、1年後の自分への愛であるといううことなのです。そして、それは同時にそのコードを触る他のメンバーへの愛でもあります。例えば、リリースしてから実際にバグがおきてしまったとしましょう。バグが起きればそれは修正されます。そのときにバグが起きるケースをテストで書き残しておくのです。こうすることで、一度自分が通ったバグを他のメンバーが踏まないようにすることができます。他のメンバーが自分と同じ轍を踏まない様にできるわけです。
また、テストコードは非エンジニアのためにも重要です。1年前に意図して実装したコードがあり、その意図をディレクターやデザイナーなどの非エンジニアも忘れているかもしれません。仕様書に書かれていたからと言ってそれを記憶しているとは限りません。その仕様書の存在すら忘れ去られていることもあります。仕様書は動かすことができません。テストコードはまさしく実際に動くコードで、記憶を記録するためのものであると言えます。人はどんどん忘れていく生き物です。それが人間の性です。忘れていくことで新しいことを記憶することができます。そして新たな価値を生み出すことができるのです。なので、過去のことは記録してどんどん忘れていってよいのです。テストコードを書くことで記録していきましょう。
自動テストは愛なのです。