RSpecでRailsアプリのモデルをテストしてみる
railsであまりテストができていなかったので、rspecを学びました。 長々としていますが、あくまで初めてRSpecを触る人を意識して書きましたので、割と噛み砕いたつもりです。 まだまだ勉強中なので、問題点があれば、ご指摘ください。
RSpecの用意
# gemfile group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] # Adds support for Capybara system testing and selenium driver gem 'capybara', '~> 2.13' gem 'selenium-webdriver' # この辺りに・・・・・・ gem 'rspec-rails', '~> 3.7.2' end
bundle installの後、 rails g rspec:installを行うと、 アプリのフォルダにspecというフォルダが作成されますので、 その中にrails_helper.rbとspec_helper.rbが存在することを確認してください。
また、アプリのフォルダの直下(Gemfileもあるところ)に.rspecというファイルができますので、そこに、
# .rspec --require spec_helper --format documentation
という感じに、--format documentationを書き加えておいてください。 これにはテストの実行結果の表示を読みやすくする目的があります。
モデルスペックを書き始める
モデルでのテストを実行する場合、ターミナル上で以下を実行します。
# terminal $ rails g rspec:model model_name
model_nameとなっているところに、テスト対象のモデル名が入ります。
実行後、 your_app/spec/の下に、models/model_name_spec.rbが作成されます。
# model_name_spec.rb require 'rails_helper' RSpec.describe ModelName, type: :model do pending "add some examples to (or delete) #{__FILE__}" end
上のような中身になっていると思いますので、ここにテストを書いていきます。
ここからは、rspec用に僕が作成したアプリ(github参照)でのテストをサンプルとして、テストを紹介していきます。 簡潔に説明すると、このアプリには、記事を登録できるArticleモデルがあり、登録したUserのみがArticleを作成することができるようになっています。 また、未登録のゲストユーザーは、indexのページにしかアクセスできないようになっています。
# rspec_test/spec/models/article_spec.rb require 'rails_helper' RSpec.describe Article, type: :model do # 記事の題名、本文、外部キー(user_id)があれば有効。 it "is valid with title, text" do article = Articles.new( title: "加藤純一", text: "加藤純一? 神" ) expect(article).to be_valid end end
上記はArticleモデルのインスタンスを作成する時のテストです。 コントローラー上ではcreateをしている段階です。
itの後の""の中身は任意の名前をつけられます。 何のテストを行なっているのかがわかる名前にした方が良いです。
article = Article.newでテストとして作成するインスタンスをarticleに格納しています。 インスタンスの中身は()の中で定義されています。title, textはArticleにある要素です。 もしすでにyour_app/app/models/model.rbでテスト対象モデルの要素が、 validates :attribute, presence: true となっている場合、テストでは()の中身にそれらの要素を必ず定義しなければなりません。
expect(article).to be_validは、英語で解釈するとそのままの意味になります。 つまり、 expect pin to be valid pinが有効であることを期待する。 という意味です。 expect().toは固定で、この構文はrspecではどこでも使います。 ()の中にテスト対象を入れます。 be_validの部分はマッチャと言って、rspecで利用することができるマッチャにはさまざまなものがあります。 マッチャに関しては以下を参考にしてみてください。 https://qiita.com/jnchito/items/2e79a1abe7cd8214caa5
それでは、作成したテストを実行してみます。 bundle exec rspecで実行できます。
# terminal Failures: 1) Pin is valid with a pin_name, address and description Failure/Error: expect(pin).to be_valid expected #<Pin id: nil, pin_name: "テスト", address: "渋谷駅", latitude: 0.356580339e2, longitude: 0.1397016358e3, description: "休日の買い物", user_id: nil, created_at: nil, updated_at: nil> to be valid, but got errors: User must exist, User can't be blank # ./spec/models/pin_spec.rb:25:in `block (2 levels) in <top (required)>'
エラーが出ました。 ご丁寧にエラーの原因を指摘してくれるので、それを参考にしましょう。 エラーによると、 got errors: User must exist, User can't be blank ということらしいので、ユーザーの情報を入れないといけないことがわかります。
これは、先ほど僕がアプリについて説明した、登録したUserのみがPinを作成できる(つまり、pinsテーブルにはuser_idという外部キーが含まれているということ)、という仕様によって引き起こされたエラーで、Pinにある外部キーがnilになっていたために生じてしまったのでした。 ですので、その外部キーであるuser_idを埋めてあげればこのエラーは直るというわけです。
FactoryBotを利用する
user_idを入力するためには、まずUserを作らなければなりません。 ですが、article_spec.rbのなかではUser.newをすることができないので(飽くまでarticleに関するarticle_spec.rbなので)、どうにかしてUserの情報を外部から引っ張ってくる必要がありそうです。
そういう時に便利なのが、FactoryBotです。 FactoryBotは、テストで利用されるであろうインスタンスを前もって作成し、保管しておいてくれる倉庫のようなものです。 必要なインスタンスがあれば、逐一この倉庫から取ってきて、テストに利用することができるのです。
# gemfile gem 'rspec-rails', '~> 3.7.2' # 先ほど追加したrspecの下あたりに・・・・・・ gem 'factory_bot_rails' end
bundle installしてから、以下のコマンドを入力してください。
# terminal $ rails g factory_bot:model User
そうすると、specフォルダの中にfactoriesフォルダが作成され、 user.rbのファイルが生成されます。
# spec/factories/users.rb FactoryBot.define do factory :user do end end
users.rbにuserのサンプルを書いていきます。
# spec/factories/users.rb FactoryBot.define do factory :user do email: "test@user" password: "000000" end end
ここでは、emailが"test@user"で、passwordが"000000"の:userを作成しました。
これで準備が整いました! では、作成したuserを利用してモデルスペックを書き直してみます。
FactoryBotの導入で詰まったら、以下のページも参考にしてみてください。 https://qiita.com/Ushinji/items/522ed01c9c14b680222c
モデルスペックを書き直す
先ほどのmodels/article_spec.rbに書き足してみます。
# rspec_test/spec/models/article_spec.rb require 'rails_helper' RSpec.describe Article, type: :model do # 記事の題名、本文、外部キー(user_id)があれば有効。 it "is valid with title, text and user_id" do user = FactoryBot.create(:user) article = user.articles.build( title: "加藤純一", text: "加藤純一? 神", user_id: 1 ) expect(article).to be_valid end end
ここではまず、Articleを作成する前に、FactoryBotで定義した:userを作成して、変数userに格納しています。 それから、作成したArticleのuser_idを1に設定しています。 userはFactoryBotで定義したものしかないので、もちろんidが1になっています。 また、あとで説明しますが、ここではuser_idを明示しなくてもテストに問題はありません。
これで実行すると、テストが無事通ることを確認できます。 基本的な使い方は以上です!
あとはそれぞれのテストに適したマッチャを利用していけば、問題なくモデルテストを終えることができると思います。 検索かけるといろいろ出てくるので、自力で見つけてみてください。
rspecを勉強するなら、この本はおすすめです。 https://leanpub.com/everydayrailsrspec-jp
それと、テストが増えてくると、userを必要とするインスタンスを毎回作ることになったりするので、その都度userも毎回作っていかなくてはなりません。 インスタンスは毎回中身が同じではない可能性もありますが、userの基本情報は固定的でしょうし、それを毎度書くのも煩わしいと思うので、その時にはbeforeを使うようにしましょう。
# pinning_place/spec/models/pin_spec.rb require 'rails_helper' RSpec.describe Pin, type: :model do before do @user = FactoryBot.create(:user) end # pin_name, address, description, user_idが揃って入れば通るテスト it "is valid with a pin_name, address, description and user_id" do user = @user pin = Pin.new( pin_name: "テスト", address: "渋谷駅", description: "休日の買い物", user_id: 1 ) expect(pin).to be_valid end end
# spec/factories/users.rb FactoryBot.define do factory :user do email: "test@user" password: "000000" end end
上のように、beforeの中に前もって@userを定義しておくと、後のテストでFactoryBot.create(:user)と書かずに、@userとだけ書けば、それだけでFactoryBotで定義しておいたインスタンスを呼び出すことができるようになります。
最後にいくつかの例となるテストを書き残しておきます。
モデルテストの例
# rspec_test/spec/models/article_spec.rb RSpec.describe Article, type: :model do before do @user = FactoryBot.create(:user) @another_user = FactoryBot.create(:another_user) end # factory_botが有効かどうかを検査。 it "has a valid factory of user" do user = @user expect(user).to be_valid end it "has a valid factory of another_user" do user = @another_user expect(user).to be_valid end # 記事の題名、本文、外部キー(user_id)があれば有効。 it "is valid with title, text and user_id" do user = @user article = user.articles.build( title: "加藤純一", text: "加藤純一? 神", user_id: 1 ) expect(article).to be_valid end # 記事の題名がなければ無効。 it "is invalid without title" do article = Article.new(title: nil) article.valid? expect(article.errors[:title]).to include("can't be blank") end # 記事の本文がなければ無効。 it "is invalid without text" do article = Article.new(text: nil) article.valid? expect(article.errors[:text]).to include("can't be blank") end # 外部キーがなければ記事は無効。 it "is invalid without user_id" do article = Article.new(user_id: nil) article.valid? expect(article.errors[:user_id]).to include("can't be blank") end # 同一のユーザーは同一の記事の題名、本文、外部キーを有する記事を作成できない。 it "does not allow a single user to have articles which has the same title" do user = @user user.articles.create( title: "加藤純一", text: "加藤純一? 神", user_id: 1 ) article = user.articles.build( title: "加藤純一", text: "加藤純一? 神", user_id: 1 ) article.valid? expect(article.errors[:title]).to include("has already been taken") end # 同一のユーザーは同一の記事の本文を有する記事を作成できない。 it "does not allow a single user to have articles which has the same text" do user = @user user.articles.create( title: "加藤純一", text: "加藤純一? 神", user_id: 1 ) article = user.articles.build( title: "加藤純一", text: "加藤純一? 神", user_id: 1 ) article.valid? expect(article.errors[:text]).to include("has already been taken") end # 異なるユーザーはそれぞれ同一の記事の題名を持つ記事を作成できる。 it "does allow each user to have an article which has the same title" do user = @user user.articles.create( title: "加藤純一", text: "純をよろしく頼む。", user_id: 1 ) another_user = @another_user article = another_user.articles.build( title: "加藤純一", text: "純をよろしく頼む。", user_id: 2 ) expect(article).to be_valid end end
FactoryBotのusers.rbにも追加しました。
# rspec_test/spec/factories/users.rb FactoryBot.define do factory :user, class: User do email "test@user" password "000000" end factory :another_user, class: User do email "test@another_user" password "000000" end end
この記事では、user_idをテストのarticleを作成する時に明示して書いていますが、これはなくても問題ありません。 ですが、その場合、
# article_spec.rb # 記事の題名、本文、外部キー(user_id)があれば有効。 it "is valid with title, text and user_id" do user = @user article = Article.new( title: "加藤純一", text: "加藤純一? 神" ) expect(article).to be_valid end
ではなく、
# article_spec.rb # 記事の題名、本文、外部キー(user_id)があれば有効。 it "is valid with title, text and user_id" do user = @user article = user.articles.build( title: "加藤純一", text: "加藤純一? 神" ) expect(article).to be_valid end
とするようにしてください。
2つの違いは、Article.newかuser.article.buildですが、これは大きく異なります。 前者はatrticleが誰の手によって作られたかが不明なためエラーになりますが、 後者はuserがarticleを作ったことを明示しています。 それゆえに、後者はbuildの引数に、わざわざuser_idを明示しなくても、自動的にuser_idを把握してくれています。 前者は必ずuser_idを指定してあげないといけないわけです。
上記のコードはgithubでも見られます。 https://github.com/yutaro1204/rspec_demonstration