Edited at

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


初めに

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の場合はいらないみたい。

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