Help us understand the problem. What is going on with this article?

RailsでのRSpecの設定やら書き方とかいろいろ

More than 1 year has passed since last update.

初めに

Ruby on Railsのテストにはmini testではなくrspecが用いられることが多いようで実際に会社で働こうと思ったらrpsecについて知っておかなくてはいけないと思い勉強したことの備忘録。

いろいろ間違っているかも

設定

とりあえずgemfileに

gem 'rspec-rails'

と書いてbundle installして

rails g rspec:install

を行えばrspecに関係するファイルが作られる。

config/application.rbに

config.generators do |g|
  g.test_framework :rspec
end

と書くとモデルやらコントローラーやらを作成したときにそれに関係するrspecのファイルを作ってくれる。

逆にrspecのファイルを自動的に作ってほしくないときは

config.generators do |g|
  g.test_framework false
end

とかいたらok

controllerに関するrpsecのファイルは作成しなくてよいけど他のは作ってほしいみたいなときは

config.generators do |g|
  g.test_framework :rspec,
  controller_specs: false
end 

みたいに設定できる

rspecファイルを作りたい場合は

rails g rspec:model user

みたいな感じで書く。この場合はuser のmodelに対するrspecのファイルを作成する。

ほかにも

rails g rspec:job users
rails g rspec:view users
rails g rspec:helper users
rails g rspec:feature users

みたいなかんじでできる。

ルートディレクトリに.rspecというファイルが作成されているがそこでrspecの出力に関する設定ができる。

とりあえず

--require spec_helper
--color
--format d

こうしたらいい感じになる。

rspecの実行に関しては

rspec
もしくは
bundle exec rspec

でいけるとおもう。

具体的になにをするか先に書く

こんな感じでなにをするかさきに書く

user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do

  it "名前とメールアドレスとパスワードがあれば登録できる"

  it "名前がなければ登録できない"

  it "メールアドレスがなければ登録できない"

  it "メールアドレスが重複していたら登録できない"

  it "パスワードがなければ登録できない"

  it "パスワードが暗号化されているか"


end

この状態で実行すると

User
  名前とメールアドレスとパスワードがあれば登録できる (PENDING: Not yet implemented)
  名前がなければ登録できない (PENDING: Not yet implemented)
  メールアドレスがなければ登録できない (PENDING: Not yet implemented)
  メールアドレスが重複していたら登録できない (PENDING: Not yet implemented)
  パスワードがなければ登録できない (PENDING: Not yet implemented)
  パスワードが暗号化されているか (PENDING: Not yet implemented)

Pending: (Failures listed here are expected and do not affect your suite's status)

  1) User 名前とメールアドレスとパスワードがあれば登録できる
     # Not yet implemented
     # ./spec/models/user_spec.rb:5

  2) User 名前がなければ登録できない
     # Not yet implemented
     # ./spec/models/user_spec.rb:7

  3) User メールアドレスがなければ登録できない
     # Not yet implemented
     # ./spec/models/user_spec.rb:9

  4) User メールアドレスが重複していたら登録できない
     # Not yet implemented
     # ./spec/models/user_spec.rb:11

  5) User パスワードがなければ登録できない
     # Not yet implemented
     # ./spec/models/user_spec.rb:13

  6) User パスワードが暗号化されているか
     # Not yet implemented
     # ./spec/models/user_spec.rb:15


Finished in 0.00456 seconds (files took 0.95882 seconds to load)
6 examples, 0 failures, 6 pending

みたいに出る。

これので一つ一つテストを成功させていく。

データ作成をfactory_bot_railsで行う。

テストに使うデータの作成にfactory_bot_railsというgemを使う

とりあえず

gem 'factory_bot_rails'

bundleする。

spec/rails_helper.rbに

config.include FactoryBot::Syntax::Methods

と書いておく

コマンドで

rails g factory_bot:model user

と書けば必要なファイルが作成される

factories/users.rbに

users.rb
FactoryBot.define do 
    factory :user do 
        name {"hiro"}
        sequence(:email) { |n| "hiro#{n}@example.com"}
        password {"password"}
    end 
end 

こんな感じで書いておくと

user = FactoryBot.create(:user)

でデータを作成してくれる

なぜemail {"hiro@example.com"}ではないかというと

emailは一意制約を付けているので

user1 = FactoryBot.create(:user)
user2 = FactoryBot.create(:user)

とするとエラーが出てしまう

sequence(:email) { |n| "hiro#{n}@example.com"}

とかけばエラーが避けられる

spec/models/user_spec.rbに具体的な処理を書く

require 'rails_helper'

RSpec.describe User, type: :model do

  it "名前とメールアドレスとパスワードがあれば登録できる" do 
    expect(FactoryBot.create(:user)).to be_valid
  end 

  it "名前がなければ登録できない" do 
    expect(FactoryBot.build(:user, name: "")).to_not be_valid 
  end 

  it "メールアドレスがなければ登録できない" do 
    expect(FactoryBot.build(:user, email: "")).to_not be_valid 
  end 

  it "メールアドレスが重複していたら登録できない" do 
    user1 = FactoryBot.create(:user,name: "taro", email: "taro@example.com")
    expect(FactoryBot.build(:user, name: "ziro", email: user1.email)).to_not be_valid
  end 

  it "パスワードがなければ登録できない" do 
    expect(FactoryBot.build(:user, password: "")).to_not be_valid 
  end 

  it "パスワードが暗号化されているか" do 
    user = FactoryBot.create(:user)
    expect(user.password_digest).to_not eq "password"
  end 

  it "password_confirmationとpasswordが異なる場合保存できない" do 
    expect(FactoryBot.build(:user,password:"password",password_confirmation: "passward")).to_not be_valid 
  end 


end

とりあえずbundle exec rspecをすると....

User
  名前とメールアドレスとパスワードがあれば登録できる (FAILED - 1)
  名前がなければ登録できない (FAILED - 2)
  メールアドレスがなければ登録できない (FAILED - 3)
  メールアドレスが重複していたら登録できない (FAILED - 4)
  パスワードがなければ登録できない (FAILED - 5)
  パスワードが暗号化されているか (FAILED - 6)

Failures:

  1) User 名前とメールアドレスとパスワードがあれば登録できる
     Failure/Error: expect(FactoryBot.create(:user)).to be_valid

     NoMethodError:
       undefined method `password=' for #<User:0x00007fffbd4d20f0>
       Did you mean?  password_digest=
     # ./spec/models/user_spec.rb:6:in `block (2 levels) in <top (required)>'

  2) User 名前がなければ登録できない
     Failure/Error: expect(FactoryBot.create(:user, name: "")).to_not be_valid

     NoMethodError:
       undefined method `password=' for #<User:0x00007fffbd5462c0>
       Did you mean?  password_digest=
     # ./spec/models/user_spec.rb:10:in `block (2 levels) in <top (required)>'

  3) User メールアドレスがなければ登録できない
     Failure/Error: expect(FactoryBot.create(:user, email: "")).to_not be_valid

     NoMethodError:
       undefined method `password=' for #<User:0x00007fffbd54d070>
       Did you mean?  password_digest=
     # ./spec/models/user_spec.rb:14:in `block (2 levels) in <top (required)>'

  4) User メールアドレスが重複していたら登録できない
     Failure/Error: user1 = FactoryBot.create(:user,name: "taro", email: "taro@example.com")

     NoMethodError:
       undefined method `password=' for #<User:0x00007fffbc254b08>
       Did you mean?  password_digest=
     # ./spec/models/user_spec.rb:18:in `block (2 levels) in <top (required)>'

  5) User パスワードがなければ登録できない
     Failure/Error: expect(FactoryBot.create(:user, password: "")).to_not be_valid

     NoMethodError:
       undefined method `password=' for #<User:0x00007fffbd56fa80>
       Did you mean?  password_digest=
     # ./spec/models/user_spec.rb:23:in `block (2 levels) in <top (required)>'

  6) User パスワードが暗号化されているか
     Failure/Error: user = FactoryBot.create(:user)

     NoMethodError:
       undefined method `password=' for #<User:0x00007fffbd57b6a0>
       Did you mean?  password_digest=
     # ./spec/models/user_spec.rb:27:in `block (2 levels) in <top (required)>'

Finished in 0.01216 seconds (files took 0.9921 seconds to load)
6 examples, 6 failures

Failed examples:

rspec ./spec/models/user_spec.rb:5 # User 名前とメールアドレスとパスワードがあれば登録できる
rspec ./spec/models/user_spec.rb:9 # User 名前がなければ登録できない
rspec ./spec/models/user_spec.rb:13 # User メールアドレスがなければ登録できない
rspec ./spec/models/user_spec.rb:17 # User メールアドレスが重複していたら登録できない
rspec ./spec/models/user_spec.rb:22 # User パスワードがなければ登録できない
rspec ./spec/models/user_spec.rb:26 # User パスワードが暗号化されているか

こんな感じでエラーがでる。

モデルファイルを修正してエラーが出ないようにする

models/user.rbに

class User < ApplicationRecord
    has_secure_password

    validates :name, presence: true
    validates :password, presence: true
    validates :email, presence: true 
    validates :email, uniqueness: true
end

こう書けばエラーが消えで全部緑で表示される:sunny:

基本的にexpect()のところに検証したいものを入れて

そのあとにbe_validであれば有効か無効かを検証したり
eqであれば等しいかどうかを検証する。

expect().toのところをto_notみたいにもできる

FactoryBot.create FactoryBot.buildの違い

buildはデータを実際に保存する必要がない場合や属性の検証のときに使う。
今回の場合は全部buildで大丈夫

let

letを使えば何回も同じコードを書かないですむ
例えば

let(:user) { FactoryBot.build(:user) }

とすればそのあとは
userと書くだけで{ FactoryBot.build(:user) }が実行されてデータを作ってくれる。

let!

let!にするとその場でテストデータを作ってくれる。
letの場合はそれが呼び出される時にコードを実行する

before

beforeはテストが実行される前に行われるもので基本的なデータをbeforeで作ったりどのテストでも行われているログイン処理とかをかいてテストを簡潔にできる。

書き方は

before do
  @user = User.new(name: "test", email: "test@gmail.com", password: "password")
 @user.save
end

みたいな感じ。

個人的にモデル作成に関してはletを使えばいいかなと思う
ログイン処理とかに関してはbeforeを使うってかんじなのかな?わからないけど....

FeatureSpec と System Spec

昔はFeatureSpecが使われてて今っぽいのがSystemSpecらしい

具体的に何をするものかというと

例えばユーザー登録画面を開いて入力欄に文字をいれて登録ボタンをおしてユーザー登録が成功したか

みたいなのを検証するものがsystem specでそういったテストを統合テストとよぶ

systemspecを行おうまえにseleliumwebdriverやらchromedriverやらの設定が必要

それに関してはこちらを参考にしてください

EC2 UbuntuでGoogle Chromeをヘッドレス実行してスクリーンショットを採取する手順
https://qiita.com/shinsaka/items/37436e256c813d277d6d

ページにアクセスする場合は

visit root_path

みたいに書いて

入力は

fill_in "Name", with: "Hiroto"
fill_in "Email", with: "hiroto@gmail.com"

みたいな感じ

ボタンクリックは

click_button "Create user"

見たいな感じ

たしか<%= f.label :*** %>のところに書いてある文字をfill_inで指定してwithで入力する文字を指定する感じ

id="***"で指定することもできる

検証の仕方は

expect(page).to have_content "hello world"

で訪れたページにhello worldという文字があるか検証できる

ユーザー登録ができているか確認する場合はこんな感じ

users_spec.rb
require 'rails_helper'

describe 'ユーザー登録', type: :system do 

    before do
        driven_by :selenium_chrome_headless
    end

    it "ユーザー登録ができるか" do

        expect {
        visit new_user_url
        fill_in "Name", with: "yuki"
        fill_in "Email", with: "yuki@gmail.com"
        fill_in "Password", with: "password"
        fill_in "Password confirmation", with: "password"
        click_button "submit"
        expect(page).to have_content "index"
        }.to change(User,:count).by(1)
    end 


end 

ここで重要なのがexpect {}の中にexpect(page).to have_content "index"と書いている

これを書かないとclick_buttonが押された後処理が実行される前にchange(User,:count).by(1)を検証してしまうためテストが失敗する。

大体こんな感じだと思う。

feature specを使ったときはitのところにscinarioと書いたりdatabase_cleanerをgem installしたりしたけどsystem specの場合はいらないみたい。

なにか間違っているところがあればご指摘いただけるとありがたいです。

sibakenY
大学卒業後Ruby, Ruby on Railsを勉強しています。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away