dreamin' blog

この記事は Ruby on Rails Advent Calendar 2015 - Qiita の12日目の記事です。

vimでrailsを書く毎日を送っているkoheisgと申します。

一昨年までphperだった自分が、この1年railsを書いて身に付けたテストの間合いについて、書きたいと思います。
rspecアドベンドカレンダーかと思いましたが、rspecでrailsをテストするときの内容なのでご容赦ください。

そもそも、テストの間合いとは

twitterでtwadaさんが下記のようなことを呟いていました。

自分にはこの間合いという言葉非常にしっくりきました。
武道経験のある自分からすると、自分の間合いで相手と戦うと相手の動きが直感的に読める(見える)ため、手数を出さずに相手を封じ込むことができます。

自分はrailsでのテストの間合いが見えてきたかもしれないと思ったのでした。
典型的なrailsのアプリケーションを書く場合は、テストコードを少なめに実装をTDDで仕留められてきたのです。

では、どのようにテストを少なくしているかを紹介します。

テストコードを少なくする技術

Routingのテストを少なくする技術

自分がrailsアプリを書く場合は、Routingに対してはほとんどテストを書きません。

そもそもrailsのroutingは、RESTの思想に則っており、

Rails.application.routes.draw do
  resources :posts
end

と書くだけで、下記のroutingが設定されます。

$ rake routes
   Prefix Verb   URI Pattern               Controller#Action
    posts GET    /posts(.:format)          posts#index
          POST   /posts(.:format)          posts#create
 new_post GET    /posts/new(.:format)      posts#new
edit_post GET    /posts/:id/edit(.:format) posts#edit
     post GET    /posts/:id(.:format)      posts#show
          PATCH  /posts/:id(.:format)      posts#update
          PUT    /posts/:id(.:format)      posts#update
          DELETE /posts/:id(.:format)      posts#destroy

ここでは posts というリソースに対して、様々な操作が設定されているのです。いわゆるCRUDと呼ばれるものであれば、これでもの足ります。

しかし、削除が必要なかったり、検索が必要だったりすることはよくあると思います。

Rails.application.routes.draw do
  resources "posts", except: [:destroy] do
    collection do
      get :search
    end
  end
end

と書けば、削除がなくなり、検索が作られます。

$ rake routes
      Prefix Verb  URI Pattern               Controller#Action
search_posts GET   /posts/search(.:format)   posts#search
       posts GET   /posts(.:format)          posts#index
             POST  /posts(.:format)          posts#create
    new_post GET   /posts/new(.:format)      posts#new
   edit_post GET   /posts/:id/edit(.:format) posts#edit
        post GET   /posts/:id(.:format)      posts#show
             PATCH /posts/:id(.:format)      posts#update
             PUT   /posts/:id(.:format)      posts#update

これらを思った通りかどうかを検証したくなり、テストを書きたくなる気持ちはわかります。
しかし、自分はここではroutingのテストを書かないようにしています。

feature specでそれぞれのroutesのpashに対して、アクセスを行い、
URLが正しいことを担保するようにしています。

    describe 'GET /posts/new' do
      before do
        visit new_post_path
      end

      it { expect(current_url).to eq 'http://www.example.com/posts/new' }
    end

もし、contorollerにroutingが当たっていないメソッドがある場合は、 rails_best_practices のgemが怒ってくれると思います。
このような静的解析も積極的に取り入れるとよいと思います。

railsbp/rails_best_practices

/rails_sample/config/routes.rb:2 - restrict auto-generated routes posts (except: [:show, :destroy])

ただし、feature specは非常に重いテストなので、urlを検証するだけのためにテストをすると実行時間が長くなってしまう問題もあります。
実行時間はもちろん短い方が良いですが、自分は実装中のテストコードの少なさには優らないと思っていますので、これらのコードは後からroutingのテストを書いて、実行時間を短くするようにするのが良いと思っています。(あくまで自分流ということで

Controllerのテストを少なくする技術

Controllerについても、Routing同様にほとんどテストをしないように設計を心がけています。

Controllerの実装を厚くしないという話は一度は誰も聞いたことがあると思います。私もそれを可能な限り実践していて、Controller内は分岐の数を極限まで少なくしています。

そうすることで、controllerのテストを、 feature spec に移譲することができます。

request specの間合いは難しいところですが、DOM構造までテストしたりはしないように、viewの表示が分岐によって変わる部分のみやresponseコードなどを仕様によって大雑把に書くようにしています。

Viewの細かいロジックはhelperかdecoratorに切り出す

feature specを薄くするのに、Viewの細かいロジックを見ないということが言えます。
自分は、Viewが微妙に分岐する場合はhelper/decoratorに切り出すようにしています。

helperはrailsのデフォルトの機能なのでいいと思います。
helperだとmodelの状態に結びついた処理が単体テストしずらいですよね。
そういう場合にdecoratorを使用しています。

drapergem/draper

Modelのテストを少なくする技術

validationのテスト

validateのテストは下記のように describe “#validate” の中に書くようにしています。
letにmodelの引数に渡したいものを定義して、contextの中で遅延的にletしなおします。

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

いつもこのパターンでテストを書いて、paramsのパターンを微妙に変更するようにしています。

scopeのテスト

基本的にscopeのテストはあまり書きません。複雑な条件を使わないと書けないようなものものだけ書きます。
しかしながら、scopeが複雑になっている時点で分割をするようにしています。

scope :hoge, -> {
  where(created_at: begining_of_day...end_of_day, state: "active")
}
scope :today, -> {
  where(created_at: begining_of_day...end_of_day)
}
scope :active, -> {
  where(state: "active")
}

def self.example
  active.today.try(:first)
end

このようになるべく小さくscopeを分割して、エンティティを取り出すものは、クラスメソッド化し、クラスメソッドだけをテストすることも多いです。

まとめ

このようにテストの間合いを見極め、少ないテストコードを振る舞いを担保できる設計を実現することで、来年も変更に強いアプリをゴリゴリ作っていきたいなと思います。