Help us understand the problem. What is going on with this article?

Railsエンジニアのための実践!テストことはじめ

 最近、会社の後輩や、業務としてRails未経験の友人から、テストの書き方について質問されることがありました。プロダクトコードは実際に実現したいことがあるのでそれを実現するために実装すれば良いため、イメージしやすいのですが、テストコードは最初はイメージしにくいのでしょう。
 そこで、これまでの経験を踏まえ、テストを書き始めるための基本的に知っておくべきことについて記述したいと思います。
 以下では、テストのフレームワークとしてRSpecを利用する前提で書きます。
 また、モデルを生成には、FactoryBotというライブラリを利用します。FactoryBotはRailsではモデル生成で広く利用されているライブラリです。FactoryBotの導入は他の記事に任せます。

テストを書きつづけるための準備

 これから、Railsにおけるテストについてのことはじめを述べていきますが、その前に、テストを書き続けるための準備をしておきましょう。テストは書いて終わりではありません。ソースコードを書いてクラウド上(GitHubやGitLab、Bitbucketなど)にpushしたら、自動的にこれまで書いたテストが回る様にしてそれをエンジニアが気づく仕組みを作る必要があります。そのためのツールを紹介します。
 まずは、継続的インテグレーション(CI)サービスです。有名なのはCircleCI、TravisCIなどです。自前で立ち上げる場合はJenkinsなどのオープンソースもあります。チームメンバーと相談して、どのサービスを利用するかを確認しましょう。ちなみに私は自前で立ち上げる手間を省くため、CircleCITravisCIを利用することが多いです。
 継続的にテストを回す仕組みができたのであれば、テストのカバレッジも継続的に測定してあげる必要があります。Railsであれば、simplecovというgemを利用することが多いでしょう。そして、カバレッジを可視化するためにCodeCovCode 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)
          expect(subject.errors.full_messages).to contain_exactly(
            'お名前は合計20文字以内で入力してください。'
          )
        }
      end
    end
  end
end

リスト2. OrderItemにfull_nameのバリデーションのテストの実装例

 バリデーションのテストの時のポイントは、テストしたいバリデーションにフォーカスをあてるということです。モデルのデフォルト値はFactoryBotの定義されますが、このとき基本的にはvalidな値で定義しておきます。その上で、FactoryBot.buildする際にテストしたい属性のみ値を変更させ、バリデーションを実行させます。リスト2では、family_namegiven_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)
          expect(subject.errors.full_messages).to contain_exactly(
            '苗字は15文字以内で入力してください。'
          )
        }
      end

リスト5. family_nameにバリデーションを追加した時のテストの例

 しかし、リスト4の実装では、full_nameのバリデーションも実行されてしまい、このテストはfailします。この様に、ある属性のバリデーションを追加したことで、他のバリデーションにも引っかかってしまい、期待しない挙動となってしまうことがあります。そのために、expect(subject.errors.keys).to contain_exactly(:family_name)として、errros.key:family_nameのみであることを明確にしています。ここで expect(subject.errors.keys).to include(:family_name)としている方を見かけますが、これでは、errors.keysの配列に:family_nameが含まれていることしかテストできておらず、:family_name以外が存在しないということができていません。contain_exaclyを使うことで他の属性にエラーがないということを担保できます。今回は、errors.keysの要素の順序は気にしなくても良いため、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_namefull_nameそれぞれでエラーメッセージを出し分けたいためです。family_nameは一つの属性であるため、苗字は15文字以内で入力してください。というメッセージで良いのに対し、full_nameは二つの属性であるため、お名前は合計20文字以内で入力してください。のように合計という言葉を追加したいわけです。
 詳細な内部の仕様は追っていませんが、messageを追加しないとja.activerecord.errors.models.order_item.attributes.family_name.too_longが呼ばれ、messagetoo_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_namegiven_nameを連結させただけではあまり面白みがないので、少しメソッドを拡張します。

  def full_name(space: '')
    "#{family_name}#{space}#{given_name}"
  end

リスト7. full_nameメソッドに引数を追加し拡張

 リスト7の様にspaceというキーワード引数を追加します。インスタンスメソッドfull_nameは変数spaceの値でfamily_namegiven_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_namegiven_namefamily_name_kanagiven_name_kanaがあり、法人名義を保存する属性としてcorporate_namecorporate_name_kanaがあるとします。
image.png
図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-showv-ifを用いて表示の制御を行います。
 では、図2のように、個人が選択された場合のフィールドに値が入力された時、サーバーサイドにはどのような値が送られてくるかわかりますでしょうか。
image.png
図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_namecorporate_name_kanaはどうでしょうか。サーバーサイドに値は渡ってくるでしょうか。
 答えは、Vue.js側の実装によります。v-ifで実装されている場合は渡ってきませんし、v-showで実装されている場合は値が渡ってきます。(未入力の場合は空文字("")で渡ってきます)したがって、v-showで実装した場合、個人名義なのにcorporate_namecorporate_name_kanaが保存されてしまうことがあるわけです。この様な挙動のテストは画面上だけのテストでは担保することができません。これを担保するにはレコードの保存状態まで確認する必要があると私は考えています。

テストケースはどの様に洗い出せばいいか

 モデルのテストの場合、ホワイトボックステストとして、実装の中身がわかっているという前提でテストを書けばいいので、言ってしまえばソースコードをみて分岐を確認し、C0網羅、C1網羅、C2網羅など4を考慮してテストを書いていけば良いです。では、SytemSpecの場合、どうすれば良いでしょうか。

 ここでは、ユーザーは申し込み画面で必要事項を入力したあと、確認画面へ進み、確認画面後は、ユーザーの入力した値に応じて画面の遷移が変わる場合を考えてみましょう。そして、このお申し込みには、付帯サービスなるものが存在し、離島でない場合は付帯サービスの説明を確認画面で表示し、離島の時は表示しません。また、申込者が本人または配偶者の場合のみ付帯サービスの説明を表示しますが、本人・配偶者以外の場合は表示しないという制御が必要だとします。
また、付帯サービスの説明表示とは別に、申し込み画面ではポイントカード登録をするかどうかを聞く項目があり、ポイントカード登録をするを選んだ場合は、確認画面後にポイントカードを登録する画面へ遷移するものとします。これをまとめたものが表1になります。rspec対象と書かれたカラムは後ほど説明します。

表1. パターンを網羅したもの
image.png

 さて、この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年前に意図して実装したコードがあり、その意図をディレクターやデザイナーなどの非エンジニアも忘れているかもしれません。仕様書に書かれていたからと言ってそれを記憶しているとは限りません。その仕様書の存在すら忘れ去られていることもあります。テストコードはまさしく実際に動くコードにて、記憶を記録するためのものであると言えます。人はどんどん忘れていく生き物です。それが人間の性です。忘れていくことで新しいことを記憶することができます。そして新たな価値を生み出すことができるのです。なので、過去のことは記録してどんどん忘れていってよいのです。テストコードを書くことで記録していきましょう。
 自動テストは愛なのです。


  1. ここの整理がきちんとできているエンジニアは意外と多くはありません。例えば高校数学で二次関数の最大最小を求める時に場合分けをした経験はありますでしょうか。あの場合分けを思い出してください。状況によって、二つの場合分けでよかったり、三つだったりしたと思います。実際の仕様ではもっと複雑になります。この複雑な場合分けを着実にかつ素早くできることがエンジニアの力量の一つの指標であると私は思います。 

  2. ホワイトボックステストについては、多くの文献がありますので、説明はそちらに譲ります。例えばQiitaの記事ではこちらの記事がわかりやすかったです。 

  3. describeには、多くの場合、テスト対象のメソッド名を記述しますが、Rubyでは、インスタンスメソッドには#を、クラスメソッドには.を付ける文化があります。 

  4. ソフトウェアテストにおけるカバレッジ(C0/C1/C2) 

  5. 40代のベテランエンジニアの書いたコードでした。ベテランエンジニアですら、こうした実装ミスを犯すということは私のような新米エンジニアはもっと実装をミスしているということを肝に命じておかなければなりません。 

yuyasat
Ruby on Rails, Vue.js, RDB, NW
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした