Edited at

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

この記事は 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を分割して、エンティティを取り出すものは、クラスメソッド化し、クラスメソッドだけをテストすることも多いです。


まとめ

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


この記事のライセンス



この記事はCC BY 4.0(クリエイティブ・コモンズ 表示 4.0 国際 ライセンス)の元で公開します。