RSpec導入
Railsの標準で入っているminitestではなく、実務に置いてminitestより採用率の高いRSpecを導入する。
テストの手法であるTDD(Test Driven Development),BDD(Behavior Driven Development)に関しては追々学んでいく。
各種設定
Capybara・・・webアプリケーションのブラウザ操作をシミュレーションできる
FactoryBot・・・テスト用のデータベースの作成をサポートするgem
各Gemインストール
group :development, :test do
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
gem 'rspec-rails', '~> 4.0.0'
gem 'factory_bot_rails', '~> 4.11'
gem 'capybara', '~> 2.13'
end
$bundle
$bin/rails g rspec:install
minitestのディレクトリを削除
$rm -r ./test
testの実行結果が見やすいように以下の記述を.rspec内に書いておく
--format documentation
色付け
--color
Capybarの初期準備
require 'capybara/rspec'
RSpec.configure do |config|
config.before(:each, type: :system) do
driven_by :selenium_chrome_headless
end
webdriversのセッティング
ChromeDriverをかんたんに導入してくれるwebdriversをインストールする
gem 'webdrivers'
追記
$ mkdir spec/support
$ touch spec/support/driver_setting.rb
RSpec.configure do |config|
config.before(:each, type: :system) do
# Spec実行時、ブラウザが自動で立ち上がり挙動を確認できる
# driven_by(:selenium_chrome)
# Spec実行時、ブラウザOFF
driven_by(:selenium_chrome_headless)
end
end
FactoryBotでテストデータを作成できるように準備する
通常FactoryBotをつけないと、メソッドを呼べない
user = FactoryBot.create(:user)
上の設定を追加することで、FactoryBotの記述が省略できる。
user = create(:user)
require "support/factory_bot"
RSpec.configure do |config|
# FactoryBotの呼び出し簡略化
config.include FactoryBot::Syntax::Methods
# springが原因でfactoryが正しく読み込まれないことを防ぐ
config.before :all do
FactoryBot.reload
end
end
# このコードのコメントアウトを外す
# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }
Userのファクトリを定義する
「ファクトリ(Factory)」とはデータを作成することを簡単にする仕組みのこと
FactoryBot.define do
factory :user do
name { 'テストユーザー' }
email { 'test1@example.com' }
password { 'password' }
end
end
Reviewのファクトリを定義する
FactoryBot.define do
factory :review do
title { "MyString" }
author { "MyString" }
description { "MyText" }
user
end
end
spring-commands-rspecを導入する
このgemによってテストの起動時間が早くなる
group :development do
gem 'spring-commands-rspec'
end
bundle install
し、次にbundle exec spring binstub rspec
をするとbinディレクトリにrspecファイルが生成される
#!/usr/bin/env ruby
begin
load File.expand_path('../spring', __FILE__)
rescue LoadError => e
raise unless e.message.include?('spring')
end
require 'bundler/setup'
load Gem.bin_path('rspec-core', 'rspec')
Shoulda Matchersの設定
Shoulda Mathersとは、RSpecやMinitestにワンライナーのテストのシンタックスを追加することで、簡潔かつ直感的にテストを書けるようになるgem
group :test do
gem 'shoulda-matchers'
# rails5以降の場合、
gem 'shoulda-matchers', git: 'https://github.com/thoughtbot/shoulda-matchers.git', branch: 'rails-5'
end
Shoulda::Matchers.configure do |config|
config.integrate do |with|
with.test_framework :rspec
with.library :rails
end
end
require "support/shoulda_matchers"
require_relative 'boot'
require 'rails/all'
require 'uri'
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
module BookReviewApp
class Application < Rails::Application
config.load_defaults 5.1
config.generators.template_engine = :slim
config.i18n.default_locale = :ja
config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}').to_s]
config.generators do |g|
g.test_framework :rspec,
fixtures: false,
view_specs: false,
helper_specs: false,
routing_specs: false
end
end
end
モデルテストのコードを書いてみる
specフォルダの中にmodelsというフォルダを作成して、modelsフォルダの中にuser_spec.rbファイルを作って以下のコードを書く
$ mkdir spec/models
$ touch spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
context "if name is entered" do
it 'if user registration is successful' do
end
end
context "if name is not entered" do
it 'if user registration fails' do
end
end
context 'if email is entered' do
it 'if user registration is successful' do
end
end
context 'if email is not entered' do
it 'if user registration fails' do
end
end
context "if password is entered" do
it 'if user registration is successful' do
end
end
context "if password is not entered" do
it 'if user registration fails' do
end
end
end
# テストの説明文の言語は構文によって変える
# example構文
example '日本語' do
# コードを記述する
end
# it構文
it 'English' do
# コードを記述する
end
このファイルだけをテストしたい場合、ファイルを指定する必要がある
$ bundle exec rspec spec/models/user_spec.rb
examples:存在するテスト / failures:失敗したテスト / pending:実行されなかったテスト
レビューの一覧表示機能のSystem Spec
$ mkdir spec/system/
$ touch spec/system/reviews_spec.rb
まずはコメントでSpecの大枠を作る
require 'rails_helper'
describe 'レビュー管理機能', type: :system do
describe '一覧表示機能' do
before do
# ユーザーAを作成しておく
# 作者がユーザーAであるレビューを作成しておく
end
context 'ユーザーがログインしているとき' do
before do
# ユーザーAがログインする
end
it 'ユーザーAが作成したレビューが表示される' do
# 作成済みのレビューのタイトルが画面上に表示されていることを確認
end
end
end
end
describe
には、「何について仕様を記述しようとしているのか」(テストの対象)を記述する。
一番外側のdescribeには、そのSpecファイル全体の主題を記述する。
階層の深いdescribeにはより細かいテーマを記述する。
context
は、テスト内容を「状況・状態」のバリエーションごとに分類するために利用する。
様々な条件で正しく動くか試す必要があるテストを整理して見やすくすることができる。
before
は、その領域全体の「前提条件」を実行するためのコードを記述する場所。
describeやcontext内にbeforeを記述すると、対応するdescribeやcontextの領域内のテストコードを実行する前に、beforeのブロック内に書かれたコードを実行してくれる。
it
は、期待する動作を文章と、ブロック内のコードで記述する。itの中に書いた期待する動作通りに対象が動作すれば、テストは成功となる。
※以下の順番でコードが実行される
最初のbefore
・ユーザーAを作成しておく(テストデータの準備)
・作成者がユーザーAであるレビューを作成しておく(テストデータの準備)
2つめのbefore
・ユーザーAでブラウザからログインする(前提となっているユーザーの操作をしておく)
it
・ユーザーAの作成したレビューのタイトルが画面上に表示されていることを確認
1つ1つのコメントを、実際のコードに置き換えていく
require 'rails_helper'
describe 'レビュー管理機能', type: :system do
describe '一覧表示機能' do
before do
# ユーザーAを作成しておく
user_a = create(:user, name: 'ユーザーA', email: 'a@example.com')
# 作者がユーザーAであるレビューを作成しておく
create(:review, name: '最初のレビュー', user: user_a)
end
context 'ユーザーがログインしているとき' do
before do
# ユーザーAがログインする
visit login_path
fill_in 'メールアドレス', with: 'a@example.com'
fill_in 'パスワード', with: 'password'
click_button 'ログインする'
end
it 'ユーザーAが作成したレビューが表示される' do
# 作成済みのレビューのタイトルが画面上に表示されていることを確認
expect(page).to have_content '最初のレビュー'
end
end
end
end
# テストが失敗した場合
$ bundle exec rspec spec/system/reviews_spec.rb
レビュー管理機能
一覧表示機能
ユーザーがログインしているとき
ユーザーAが作成したレビューが表示される (FAILED - 1)
Failures:
1) レビュー管理機能 一覧表示機能 ユーザーがログインしているとき ユーザーAが作成したレビューが表示される
Failure/Error: user_a = FactoryBot.create(user, name: 'ユーザーA', email: 'a@example.com')
NameError:
undefined local variable or method `user' for #<RSpec::ExampleGroups::Nested::Nested::Nested:0x00007fed26346f28>
[Screenshot]: tmp/screenshots/failures_r_spec_example_groups_nested_nested_nested_ユーザーaが作成したレビューが表示される_412.png
# ./spec/system/reviews_spec.rb:7:in `block (3 levels) in <top (required)>'
Finished in 4.19 seconds (files took 5.32 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./spec/system/reviews_spec.rb:21 # レビュー管理機能 一覧表示機能 ユーザーがログインしているとき ユーザーAが作成したレビューが表示される
レビューの新規作成機能のSystem Spec
.
.
.
describe 'レビュー新規作成機能' do
let(:login_user) { :user_a }
before do
visit new_review_path
fill_in 'タイトル', with: review_name
click_button '投稿する'
end
context '新規投稿画面でタイトルを入力したとき' do
let(:review_name) { '新規投稿のテストを書く' }
it '正常に投稿される' do
expect(page).to have_selector 'alert-success', text: '新規投稿のテストを書く'
end
end
context '新規投稿画面でタイトルを入力しなかったとき' do
let(:review_name) { '' }
it 'エラーになる' do
within '#error_explanation' do
expect(page).to have_content 'タイトルを入力してください'
end
end
end
end
end
letは「before処理でテストスコープの変数に値を代入する」のに近いイメージで利用できる機能(実際に変数を作るわけではない)
let(定義名){ 定義の内容 }
have_selectorというマッチャを使うと、HTML内の特定の要素をセレクタ(CSSセレクタ)で指定することができる
have_selector
withinというメソッドを使うと、withinのブロックの中でpageの内容を検査することで、探索する範囲を画面内の特定の範囲に狭めることができる
within '範囲を指定' do
Userモデルのテスト
コメントで大枠を作る
# validations
describe "validations" do
# 存在性
describe "presence" do
# 名前、メールアドレス
it "name and email should not ro be empty/falsy"
# パスワード、パスワード確認
context "when password and confirmation is not present" do
it "@user is invalid"
end
end
# 文字数 charesters
describe "charesters" do
# 名前: 最大 20 文字
context "when name is too long"
it "@user is inavlid"
# パスワード、パスワード確認: 最小 6 文字
describe "when password is too short"
it "@user is inavlid"
end
# email のフォーマット
describe "email format" do
# invalid なフォーマット
context "when invalid format" do
it "@user is inavlid"
# valid なフォーマット
context "when valid format" do
it "@user is valid"
end
end
end
end
コメントを埋めていく
RSpec.describe User, type: :model do
# 保留されたテンプレート
pending "add some examples to (or delete) #{__FILE__}"
# validations
describe "validations" do
# 存在性
describe "presence" do
# 名前、メールアドレス
it { should validate_presence_of :name }
it { should validate_presence_of :email }
# パスワード、パスワード確認
context "when password and confirmation is not present" do
before { user.password = user.password_confirmation = " " }
it { should_not be_valid }
end
end
# 文字数 charesters
describe "charesters" do
# 名前: 最大 20 文字, パスワード: 最小 6 文字
it { should validate_length_of(:name).is_at_most(20) }
it { should validate_length_of(:password).is_at_least(6) }
end
# email のフォーマット
describe "email format" do
# invalid なフォーマット
context "when invalid format" do
# 無効なオブジェクト
it "should be invalid" do
invalid_addr = %w[user@foo,com user_at_foo.org example.user@foo. foo@bar_baz.com foo@bar+baz.com]
invalid_addr.each do |addr|
user.email = addr
expect(user).not_to be_valid
end
end
end
end
# valid なフォーマット
context "when valid format" do
# 有効なオブジェクト
it "should be valid" do
valid_addr = %w[user@foo.COM A_US-ER@f.b.org frst.lst@foo.jp a+b@baz.cn]
valid_addr.each do |addr|
user.email = addr
expect(user).to be_valid
end
end
end
end
end
↓テスト結果↓
$ bundle exec rspec spec/models/user_spec.rb
User
add some examples to (or delete) /Users/kiyokawakouji/Rails-app/book_review_app/spec/models/user_spec.rb (PENDING: Not yet implemented)
validations
presence
is expected to validate that :name cannot be empty/falsy
is expected to validate that :email cannot be empty/falsy
when password and confirmation is not present
example at ./spec/models/user_spec.rb:17 (FAILED - 1)
charesters
is expected to validate that the length of :name is at most 20
is expected to validate that the length of :password is at least 6
email format
when invalid format
should be invalid (FAILED - 2)
when valid format
should be valid (FAILED - 3)
Pending: (Failures listed here are expected and do not affect your suite's status)
1) User add some examples to (or delete) /Users/kiyokawakouji/Rails-app/book_review_app/spec/models/user_spec.rb
# Not yet implemented
# ./spec/models/user_spec.rb:5
Failures:
1) User validations presence when password and confirmation is not present
Failure/Error: before { user.password = user.password_confirmation = " " }
NameError:
undefined local variable or method `user' for #<RSpec::ExampleGroups::User::Validations::Presence::WhenPasswordAndConfirmationIsNotPresent:0x00007fd1586e2770>
# ./spec/models/user_spec.rb:16:in `block (5 levels) in <top (required)>'
2) User validations email format when invalid format should be invalid
Failure/Error: user.email = addr
NameError:
undefined local variable or method `user' for #<RSpec::ExampleGroups::User::Validations::EmailFormat::WhenInvalidFormat:0x00007fd15831d6f8>
# ./spec/models/user_spec.rb:34:in `block (6 levels) in <top (required)>'
# ./spec/models/user_spec.rb:33:in `each'
# ./spec/models/user_spec.rb:33:in `block (5 levels) in <top (required)>'
3) User validations when valid format should be valid
Failure/Error: user.email = addr
NameError:
undefined local variable or method `user' for #<RSpec::ExampleGroups::User::Validations::WhenValidFormat:0x00007fd1582d5790>
# ./spec/models/user_spec.rb:46:in `block (5 levels) in <top (required)>'
# ./spec/models/user_spec.rb:45:in `each'
# ./spec/models/user_spec.rb:45:in `block (4 levels) in <top (required)>'
Finished in 0.61427 seconds (files took 5.94 seconds to load)
8 examples, 3 failures, 1 pending
Failed examples:
rspec ./spec/models/user_spec.rb:17 # User validations presence when password and confirmation is not present
rspec ./spec/models/user_spec.rb:31 # User validations email format when invalid format should be invalid
rspec ./spec/models/user_spec.rb:43 # User validations when valid format should be valid
以下テスト結果の解説
panding
はコメントアウトでも書かれている通り、RSpecの書き方を示しているテンプレート(今回はあえて消さずに起こしておいた)なのでスルー。
(FAILED - 数字)
がテストコードが失敗したことを示している。今回は3カ所のテストコードが失敗したことになる。
Failures:~
はテストが失敗した該当コードとエラー内容が出力されている。
Failures example:~
はテストが失敗したファイルの該当コードの行と期待する動作が出力されている。
今回はモデルのテストなのでvalidatesを確認していく
class User < ApplicationRecord
devise :database_authenticatable, :rememberable,
:validatable, :timeoutable, :registerable, :confirmable
# nameの値が空でないか?
validates :name, presence: true
# nameの値が20文字以下か?
validates :name, length: { maximum: 20 }
# emailアドレスをデータベースに保存する際にメールアドレスを小文字にする
before_save { self.email = email.downcase }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
# emailがその確認用の入力と一致しているか?
validates :email, { presence: true, format: { with: VALID_EMAIL_REGEX } }
validates :email, :encrypted_password, confirmation: true
# passwordの値が空でないか?
validates :password, presence: true
# passwordの値が6文字以上か?
validates :password, length: { minimum: 6 }
# データベース上でUserとReviewを紐づける
has_many :reviews, dependent: :destroy, foreign_key: :review_user_id
end
番外編
失敗したテストだけを再実行できる(--only-failuresオプション)の設定
RSpec.configure do |config|
config.example_status_persistence_file_path = "./spec/examples.txt"
end
------------------------------------------------------------------
gitを使っている場合は .gitignore に以下の設定も追記します。
spec/examples.txt
$ bundle exec rspec --only-failures
rspecのオプション
rspecのオプション一覧表示
$ rspec --help
テストの一覧を実行せずに出力
$ rspec --dry-run
テストの警告を出力
$ rspec --warnings
このコマンドはspecファイル中の警告を表示する。スペルミスなどがあればそれも警告の内に入れ、出力する
試しに$ rsoec --warnings
をした際に以下の出力がされた
WARN: Unresolved or ambiguous specs during Gem::Specification.reset:
diff-lcs (>= 1.2.0, < 2.0)
Available/installed versions of this gem:
- 1.4
- 1.3
WARN: Clearing out unresolved specs. Try 'gem cleanup <gem>'
Please report a bug if this causes problems.
~~省略~~
同一のgemが複数のバージョンでインストールされていて、システムでどれを扱うか決められないと言われている。
旧バージョンのgemを削除して、一つに絞るか、具体的なバージョンを決める必要がある。
テストの種類
全体的なテスト
・システムテスト(System Spec/Feature Spec)
・統合テスト(Request Spec)
・機能テスト(Controller Spec)
個々の部品テスト
・モデル(Model Spec)
・ルーティング(Routing Spec)
・ビュー(View Spec)
・ヘルパー(Helper Spec)
・メーラー(Mailer Spec)
・ジョブ(job Spec)
テストコード自体の信頼性
テストコードが意図した通りに動いていることを確認しなければならない。これは「テスト対象のコードでいろいろ試すアプローチ(exercising the code under test)」としても知られている。
誤判定ではないことを証明するためには例えば、to
をto_not
に変えてエクスペクテーションを反転させる。
# 名がなければ無効な状態であること(成功)
it "is invalid without a first name" do
user = User.new(first_name: nil)
user.valid?
expect(user.errors[:first_name]).to include("can't be blank")
end
# 名がなければ無効な状態であること(失敗)
it "is invalid without a first name" do
user = User.new(first_name: nil)
user.valid?
expect(user.errors[:first_name]).to_not include("can't be blank")
end
参考
Rails RSpecの準備とテストコード基礎
【Rails】『RSpec + FactoryBot + Capybara + Webdrivers』の導入&初期設定からテストの書き方まで
Rails + Selenium + DockerでSystemSpecの環境構築
https://github.com/everydayrails/everydayrails-rspec-2017
使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」
RSpec 設定
Rspecの便利なオプション
苦しめられてやっと理解できたRailsコールバックの使い方