dreamin' blog

railsでテストコードを少なくする技術 - TDD on railsの間合い - Qiita

以前、上記のような記事を書きました。

「Model SpecやRequest Spec/Feature Spec(今ならSystem Spec)を中心に書いて、実装速度を上げていこう」という話でした。

その後、Ruby on Railsのコードを読み書きする中で、自分自信の考えが少し変化しました。ここで一度新しい僕のRuby on Railsでのテストとの向き合い方をまとめたくなりました。

いろいろと変化している部分があり長くなるので、今回はModel SpecのValidationの書き方を以前のコードと比べつつ見ていきましょう。

以前の記事に書いてあるModel Specのコード

前述の記事のvalidationのテスト という項目のコードです。(一部改変しています)

describe Post, type: :model do
  describe '#validate' do
    subject { Post.new params }
    context '全てのデータが正しいとき' do
      let(:params) { body: "iiyon" }
      it { is_expected.to be_valid }
    end
    context '正しくない場合' do
      let(:params) { body: "dame" }
      it { is_expected.to be_valid }
    end
  end
end

validateのテストは下記のように describe “#validate” の中に書くようにしています。
letにmodelの引数に渡したいものを定義して、contextの中で遅延的にletしなおします。
いつもこのパターンでテストを書いて、paramsのパターンを微妙に変更するようにしています。

この考え方は基本的には変わりません。タイトルがテストを少なくすると言っていますが、これだと結構テストコードが増えそうですね。Modelはしっかりテストしましょうという考え方なのだということもわかります。

今の考え方が投影されたModel Specのコード

上記のコードを今ならどのように書くのかは以下です。

describe Post, type: :model do
  describe '#valid?' do
    subject { Post.new(params) }
    let(:params) { { body: body } }
    
    context 'when body is ok' do
      let(:body) { "iiyon" }
      
      it { is_expected.to be_valid }
    end
    
    context 'when body is not ok' do
      let(:body) { "dame" }
      
      it do
        expect(subject).to be_valid
        expect(subject.errors.full_messages).to eq ['Body is shold be iiyon']
      end
    end
  end
end

ほとんど以前の記事と考え方は変わってはいないのですが、変わった部分に関して解説を行っていきます。

describe に対応する subject と context に対応する let

describe '#メソッド名' の直後に subject を持ってくるのは昔から読みやすくて好きなので、変わらずですね。better specなどでも紹介されている方法です。

subject { Post.new(params) } として params を一括で let に切り出しています。ここは多少の拘りがあって、差分だけを context 内の let に持っていきたいという気持ちの表明です。
仕様変更で追加の attribute が必要になったときに、読みやすくなることを期待しています。

エラーメッセージのテスト

続いて、大きく変わった部分はvalidationがエラーになるときに、必ずmessageエラーをテストするようになりました。

これがテストできていないと、view側でエラーになったときに予期せぬ表示になってしまいます。また、i18nなどを使っているとテストができる部分がなくなってしまうので、これを書くようにしています。このテストを書くようになってから、組み込みvalidationだからといって省略することは少なくなってきました。

そもそも、Validationのテストは shoulda matcher などに任せてしまう人も多いと思います。私は shoulda matcher が使えるテストなら、Railsが用意してくれているValidation用メソッドについてはテストを省略することが多かったです。
しかし最近ではRailsが用意してくれているValidation用メソッドであったとしても、テストを書くことが増えてきました。それは前述のエラーメッセージをテストしたいという理由です。

まとめ

以前の主張と比べて、少し保守的なコードを書くようになったように感じました。
しかし、以前の記事も「テストコードを少なくしたい」という欲は見えつつも、「十分にModelはテストするように」というように読めました。

大きく変わったのはデフォルトのValidationも好んで書くようになったことでしょう。エラーメッセージをテストする効果を体感することで、しっかりと書くようになりました。

入力値のvalidationはパラメータが増えれば増えるほど、掛け合わせの数も増えますし、特にRailsのValidationは ifオプションwith_options などがでてくると一気にどんどん肥大化していきます。
肥大化させないテクニックやValidationに頼らないようにするテクニックももちろんありますが、またValidatorクラスにロジックを移譲したり、Serviceクラスにロジックを移したりする際にもテストは非常に有用になってきます。

少し保守的な主張になったのは開発を進めていく中で、バグや設計ミスなど痛い目にあった部分が、テストにフィードバックされて、テストの書き方が変わったという意味で私の成長なのかもしれません。

想定突っ込み集

以下は今回の主張したい内容と少しずれるので、まとめより下においておきます。

described_class は利用しないのですか?

described_class は利用する時もありますし、利用しない時もあります。
チームのメンバーがRSpecの特有の記述方法を使うと学習コストが高くなってしまう場合は使用しません。利用する時の気持ちとしてはクラス名に自信がないときです。レビューのタイミングや実装の後半になってテストの中まで影響が及ぶのは避けるためです。

FactoryBot は利用しないのですか?

ここで大体の実装案としては let(:params) { attributes_for(:post) } という方式でしょうか。これについては使うこともあります。しかし、FactoryBotのデフォルト値に関しては変化したときにテストが落ちることを優先したいため、あえてパラメータを自分で指定してテストする形にしています。
また subject { build(:post) } とする方法もありますが、こちらについてもValidationをテストする時に限ってはオブジェクトを自分で作ってテストをする形を採用することが多いです。

context は英語で書くようになったのですか?

英語で書くことが増えましたし、rubocop-rspec に指摘されるので、合わせてしまっています。日本語よりも目的語が見やすい気がしています。

describeが #valid? なのに subject { Post.new } なのが気持ち悪いです

わかりますが、 be_valid を使いたくて、このようにしています。
be_valid の方がテストが落ちたときの情報が多くて好きです。

突っ込み集まとめ

この辺りは好みの問題ですので、チームメンバーの意向と私の考えるメリットデメリットをすり合わせながら進めています。