今日は木曜日だったので、ハンバーグの会(Okayama.rb)に参加してきました。
今日は@mako_wisにテストの書き方について相談を受けたので、粒度とかについて説明しましたが、私は説明し始めると早口になってしまうので詰め込みすぎたかもしれないと思ったのでちょっとまとめておこうと思いました。ちなみに、書き方といってもRSpecの始め方とかではないです。その点はあしからず。
Railsプロジェクトのいいところは、テストがとてもしやすいところだと思います。
私は今の会社に入るまで、テストは書きたいけれど、どう書けばいいのかわからなかったのと、頑張って書いてみたものの、成果が周りに評価されなかったのでこのままでいいのだろうか?と思い悩んでいました。しかし、既にテストがあるプロジェクトに入って書き方を学べた事と、同僚とThe RSpec Book読書会を社内で開いて勉強したおかげで、結構綺麗に書けるようになったんじゃないかな?と思ってます。持つべき物はいい仲間と向上心です(キリッ
テスト戦略
まずテスト戦略についてです。
基本的にはテストは『開発者の不安を取り除くため』にあります。裏を返せば、不安でないところはテストを書く必要はありません。テストはやりすぎるとスピードが遅くなるし、ライブラリのテストをやっているようなことになってきます。ありそうなのは、Deviseを使ってユーザー認証作ってて、Deviseのテストになってしまっていたりとか。軽くならいいでしょうが、やりすぎはやめましょう。
どこまでテストするか?ですが、不安なところはテストしますが、重複しそうなところはやりません。
以下のようなものをテストします。
- Model(ビジネスロジックが詰まってる)
- ActiveDecorator(Modelの延長なので)
- rake task(定期処理が多いから)
- インテグレーションテスト(Capybara + PhantomJS + poltergeist)
インテグレーションテストをするので、基本的にControllerとViewのテストはそちらで賄います。ただし、絶対にテストしないわけではありません。不安ならテストします。
どこからテストするのか?Modelでしょ!
Railsでテストが一番書きやすいのは、Modelです。どうしてテストしやすいかというと、
- テスト対象が明確である
- 依存関係が少ないので取りかかりやすい
- 高速である
という点です。まずはモデルのテストから書いて、慣れていく事をオススメします。
テストを定義してみる
では実際にRSpecでモデルのテストを書いてみるとしましょう。私はテストさえ書けば、テストを先に書こうが後に書こうがどっちでもいいと思います。どちらにしても最初に想定してないパターンとか思いつくので。でもモデルはテスト駆動しやすいと思うので、先に設計のつもりで書いたりすることが多いです。
たとえば、Userモデルがあって、名前、メールアドレス、パスワードを持っていて、登録するときには確認用パスワードが必須とします。
そうなると、こういう感じでテストが書けるかと思います。
require 'spec_helper' describe User do context '正しいデータを入れた場合' do it "登録できること" end context '既にデータがある場合' do it '編集できること' it '削除できること' end describe 'エラーチェック' do context '何も入れない場合' do it 'エラーになること' end context 'メールアドレスでない場合' do it 'エラーになること' end context 'パスワードと確認用パスワードが一致しない場合' do it 'エラーになること' end end end
とりあえず考えられるパターンを先に書き出しておきます。
it文だけ書いておくとpending状態になるので、思いつくがままに書いていると、仕様がテストに書き出されてきます。ちなみにdescribeとcontextはどちらでもいいのですが、contextを使う事で、テスト対象がどういう場合かを定義しておきます。describeはいくらでもネスト(入れ子)できますが、最後はcontextにするという感じです。よく使うパターンだと、
- describe・・・〜で、〜であり
- context・・・〜の場合
- it・・・〜であること、〜でないこと
で終わります。
テストを実装してみる
では、次に、正しいデータを入れた場合、のテストを実装してみましょう。
context '正しいデータを入れた場合' do it "登録できること" do user = User.new(name: 'パトラッシュ', email: 'patorash@email.com', password: 'password', password_confirmation: 'password') expect(user.save).to be_true end end
普通に書くと、上のようになります。しかし、これから次は編集、削除のテストもあるのに、毎回User.newやUser.createを書くのは面倒です。そこで、テスト用データを作成しておきます。
FactoryGirlを使おう!
テスト用データの作成ですが、私はFactoryGirlを使って作っています。FactoryGirlは多機能なので色々できますが、まずはシンプルに使いましょう。
FactoryGirl.define do factory :user do |d| d.name 'パトラッシュ' d.email 'patorash@email.com' d.password 'password' d.password_confirmation 'password' end factory :user_invalid_password, parent: :user do |d| d.password 'invalid_password' d.password_confirmation 'wrong_password' end end
親データを指定して、一部の値を書き換えたりすることができるので、想定できるパターンのユーザーを都度作成しておくと、どういう意図のテストデータかわかりやすくなります。例えばuser_invalid_passwordは、パスワードが違うテストデータという事になります。
FactoryGirlを使ってテストを直してみる
では、FactoryGirlを使って直してみます。
context '正しいデータを入れた場合' do it "登録できること" do user = FactoryGirl.build(:user) expect(user.save).to be_true end end
とてもシンプルになりました。
テスト対象を明確にしよう
さっきの修正でも十分機能はしていますが、さらに上を目指しましょう。何をテストしているのかを明確にするべきです。RSpecでは、テスト対象のオブジェクトを、subjectに設定することができます。こうすると、テスト内でsubjectと書くと、対象のオブジェクトにアクセスできます。
context '正しいデータを入れた場合' do subject { FactoryGirl.build(:user) } it "登録できること" do expect(subject.save).to be_true end end
なんか、逆に長くない?と思われたかもしれません。そういうこともあります。ありますが、subjectがテスト対象であるというのが明確になりました。また、subjectに設定すると、英語圏の人はテストが書きやすくなります。日本語圏の我々は上の書き方でいいですが、一応紹介しておきます。
context '正しいデータを入れた場合' do subject { FactoryGirl.build(:user) } its(:save) { should be_true } end
好きな方で書きましょう。私はどっちも使います。
次のテストをやってみよう!
次のテストは編集と削除です。事前にデータを登録することになりますが、これもFactoryGirlを使えば簡単です。
context '既にデータがある場合' do before do @user = FactoryGirl.create(:user) end it '編集できること' do @user.name = "ネロ" expect(@user.save).to be_true end it '削除できること' do expect(@user.destroy).to be_true end end
beforeを使えばテストの事前処理が、afterを使うとテストの後処理が定義できます。beforeは、:each, :allがあります(:aroundもあった気がする…)。省略すると、:eachになります。:eachは、テストのitが実行される毎に実行されます。:allは、そのフォーカスの中で1度だけ実行されます。基本的には、:eachを使います。:allはRSpecの事前定義とかで使います。
さて、勘のいい人は気付いたかもしれませんが、これ、subjectを使うともっと短くできます。
context '既にデータがある場合' do subject { FactoryGirl.create(:user) } it '編集できること' do subject.name = "ネロ" expect(subject.save).to be_true end it '削除できること' do expect(subject.destroy).to be_true end end
テストシナリオを増やす
午前3時過ぎてたので、続きはまた後で書きます。