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をしている段階です。

  1. itの後の""の中身は任意の名前をつけられます。 何のテストを行なっているのかがわかる名前にした方が良いです。

  2. article = Article.newでテストとして作成するインスタンスをarticleに格納しています。 インスタンスの中身は()の中で定義されています。title, textはArticleにある要素です。 もしすでにyour_app/app/models/model.rbでテスト対象モデルの要素が、 validates :attribute, presence: true となっている場合、テストでは()の中身にそれらの要素を必ず定義しなければなりません。

  3. 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