この記事は Ruby on Rails Advent Calendar 2015 - Qiita の12日目の記事です。
vimでrailsを書く毎日を送っているkoheisgと申します。
一昨年までphperだった自分が、この1年railsを書いて身に付けたテストの間合いについて、書きたいと思います。
rspecアドベンドカレンダーかと思いましたが、rspecでrailsをテストするときの内容なのでご容赦ください。
そもそも、テストの間合いとは
twitterでtwadaさんが下記のようなことを呟いていました。
今日はまだテスト駆動開発についてよく知らない人や、少し知っている人、もう間合いを見定めた人、様々な人の前で TDD について改めて話しました。お聞きくださった皆様、ありがとうございました。講演後もたくさん議論できてよかったです。
— Takuto Wada (@t_wada) 2015, 10月 27
自分にはこの間合い
という言葉非常にしっくりきました。
武道経験のある自分からすると、自分の間合いで相手と戦うと相手の動きが直感的に読める(見える)ため、手数を出さずに相手を封じ込むことができます。
自分は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が怒ってくれると思います。
このような静的解析も積極的に取り入れるとよいと思います。
/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を使用しています。
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を分割して、エンティティを取り出すものは、クラスメソッド化し、クラスメソッドだけをテストすることも多いです。
まとめ
このようにテストの間合いを見極め、少ないテストコードを振る舞いを担保できる設計を実現することで、来年も変更に強いアプリをゴリゴリ作っていきたいなと思います。