はじめに
- 今回はRSpecの技術書であるEveryday Rails - RSpecによるRailsテスト入門を読了したので、その要約と感想について書いていければと思います。
RSpecとは?
- ruby用に開発された、テスト用の フレームワーク のことです。
- RSpecを用いることで、少ない記述で、可読性に優れたコードを簡単に記述することができます。
本記事の対象者
- railsのチュートリアルレベルを一通り学び終え、テストコードの学習を始めようと思っている方
- これから Everyday Rails - RSpecによるRailsテスト入門 を読む前に要点を掴んでおきたい方
- 実務でRSpecを使うことになり、キャッチアップの上で全体の概要を掴んでおきた方
を対象にこの記事を執筆しました。
ぜひ最後まで閲覧いただけると幸いです。
よかったところ
RSpecを深く、網羅的に学べる「唯一」の技術書
- 表題にあるように、本書はRSpecを 抜け漏れなく体系的 に学ぶことができます。
Ruby,Railsの技術書はたくさんありますが、私が確認できる中で RSpec 1つに絞り、200ページ以上に渡って解説されている技術書は本書以外にないと思われます。
ほとんどがrails,rubyの中の1要素として20pほど割いて、基礎的な部分を触れられる程度の形になっているのが現状です。 - なので、今後より安全で強固なシステムを作るためのテストについて、より深く、網羅的に学びたいという方は本書を手元において、参考にしながらコーディングしていくの求められると思われます。
著者の実務経験を基にしたRSpecのエッセンスを学ぶことができる
- 本書は著者が実務経験をする中で、 現場で通用する実践的スキル を中心に学ぶことができる技術書になっています。
以下引用文
本書の第二のゴールは日常的によく使う RSpec の機能と構文をあなたが使いこなせるように手助けすることです。
〜中略〜
Rails アプリケーションをテストする際に私が何年にもわたって使ってきたツールに焦点を絞って説明しています。
上記のあるように、実務に役立つノウハウに絞って学ぶことができ、その分難易度も高いですが、より実践的な知識を身につけることができる技術書になっています。
学んだこと
1.RailsにおけるRSpecテストとは
- Railsにはデフォルトでテストコードの雛形が生成される。
- しかし、railsのフレームワークを覚えることで手一杯になってしましい、テストまで手が回らない人が多い。
- またテストの工数を割くことで、開発工数を減らすことに抵抗がある人が多く、ブラウザ上だけのテストで終わらせてしまう人も多くいる。
- どうしても開発に重きが置かれ、テストにおける学習は軽視されがち
- Railsにおいては、RSpecを使用すると読みやすいスペックが簡単に書くことができる。
- RSpecは複雑なフレームワークだが、著者が実務を通じて重要だと感じた単元に絞って本書では説明する。
筆者が考えるテスト原則
- 絶対的に正しいテストの原則というものは存在しない
- その上で著者が掲げる原則が以下
- テストは信頼できるものであること
- テストは簡単に書けるもの
- テストは簡単に理解できること
- 一番大切なのはテストが存在すること(テストコードがないパターンは多々ある)
- ブラウザ上の動きでテストするのではなく、テストコードを書き自動化することで、開発効率を高め、潜在的なバグや境界値に潜む問題を発見することができる
2.モデルスペック
- 最初はアプリケーションのコアとなるモデルからテストをはじめる。
- railsで機能を構築するときもまずモデルから生成するので同様の手順
- 既存のモデルの対して、モデルスペックを作る。
モデルスペックの基本構造
- モデルの状態が有効(valid)になっていること
- バリデーションを失敗させるテストであれば、モデルの状態が有効になっていること
- クラスメソッドとインスタンスメソッドが期待通り動作すること
- 例)
- 期待する記述をまとめて記述している
- example(
it
で始まる1行)一つにつき、結果を一つだけ期待している。 - どのexampleも明示的である。
describe User do # 姓、名、メール、パスワードがあれば有効な状態であること it "is valid with a first name, last name, email, and password" # 名がなければ無効な状態であること it "is invalid without a first name" # 姓がなければ無効な状態であること it "is invalid without a last name" # メールアドレスがなければ無効な状態であること it "is invalid without an email address" # 重複したメールアドレスなら無効な状態であること it "is invalid with a duplicate email address" # ユーザーのフルネームを文字列として返すこと it "returns a user's full name as a string" end
should構文とexpect構文
- 過去にはshould構文を利用していたが、2012年以降はexpect構文に変わった。
- shouldはverによってはエラーになることがあるので、基本的にはexpect構文を用いる。
# should構文
it "adds 2 and 1 to make 3" do
(2 + 1).should eq 3
end
# expect構文
it "adds 2 and 1 to make 3" do
expect(2 + 1).to eq 3
end
モデルスペックのサンプルコード
-
be_valid
というメソッドを用いてモデルが有効な状態かどうか検討 - testする際は実際にテストコードやソースコードを書き換えて、わざとtestを失敗させることも必要
require 'rails_helper'
RSpec.describe User, type: :model do
# 姓、名、メール、パスワードがあれば有効な状態であること
it "is valid with a first name, last name, email, and password" do
user = User.new(
first_name: "Aaron",
last_name: "Sumner",
email: "tester@example.com",
password: "dottle-nouveau-pavilion-tights-furze",
)
expect(user).to be_valid
end
# 異常系テスト(first_nameが空欄のまま入力された場合)
it "is invalid without a first name" do
use = Use.new(first_name: nil)
user.valid?
expect(user.errors[:first_name]).to include("can't be blank")
end
end
マッチャについて
- マッチャとは
- 期待値と実際の値を比較して、一致した(もしくは一致しなかった)という結果を返すオブジェクト
- to/not_to/to_not
- 「〜であること」を期待する場合は
to
expect(1 + 2).to eq 3
- 「〜でないこと」を期待する場合は
not_to
/to_not
を使う。
expect(1 + 2).not_to eq 1 # または expect(1 + 2).to_not eq 1
- 「〜であること」を期待する場合は
-
eq
- 「等しい」かどうかを検証する場合に使用する。
expect(1 + 2).to eq 3
-
be_xxxx
- be_xxxを使用することで、テストコードが自然な英文っぽく読めるようになる。
-
empty?
のようにメソッドが「?」で終わり、戻り値が true/falseになるメソッドをbe_empty
のような形で検証することができる。
expect([]).to be_empty # 以下のコードと同様の意味になる expect([].empty?).to be true # または expect([].empty?).to eq true
-
be_valid
を使用することで、Railsのmodelにバリデーションエラーが発生していないことをテストすることができる。
user = User.new(name: 'Tom', email: 'tom@example.com') expect(user).to be_valid # user.valid? が true になればパスする
- include
- 「〜が含まれていること」を検証することができる。
expect([1, 2, 3]).to include 2
describe,context(テストのグループ化)
-
describe
- テストのグループ化を宣言するメソッド
- グループ分けすることで、テストの可読性を高めることができる。
RSpec.describe '四則演算' do # itはテストをexampleという単位でまとめる役割をする。 it '1 + 1 は 2 になること' do expect(1 + 1).to eq 2 end end
- ネストさせることも可能
RSpec.describe '四則演算' do describe '足し算' do it '1 + 1 は 2 になること' do expect(1 + 1).to eq 2 end end describe '引き算' do it '10 - 1 は 9 になること' do expect(10 - 1).to eq 9 end end end
-
context
- テストをグループ化するためのメソッド
-
describe
と機能的には同じだが、context
は条件を分けたりするときに使うことが多い。
RSpec.describe User do describe '#greet' do context '12歳以下の場合' do it 'ひらがなで答えること' do user = User.new(name: 'たろう', age: 12) expect(user.greet).to eq 'ぼくはたろうだよ。' end end context '13歳以上の場合' do it '漢字で答えること' do user = User.new(name: 'たろう', age: 13) expect(user.greet).to eq '僕はたろうです。' end end end end
before
-
処理を共通化させたい時に使用する。
-
before do ... end
で囲まれた部分はexampleの実行前に毎回呼ばれる。RSpec.describe User do describe '#greet' do before do @params = { name: 'たろう' } end context '12歳以下の場合' do it 'ひらがなで答えること' do user = User.new(**@params.merge(age: 12)) expect(user.greet).to eq 'ぼくはたろうだよ。' end end context '13歳以上の場合' do it '漢字で答えること' do user = User.new(**@params.merge(age: 13)) expect(user.greet).to eq '僕はたろうです。' end end end end
3.FactoryBot
Factory botとは
- Rspecでテストを行う際に「検証用のテストデータを生成する」Gemのこと
- 検証に必要なデータを生成し、使いまわすことが可能。
- テストが複雑になったときに、ファクトリを使えば、より可読性が高く、柔軟性の高いテストコードを記述することができる。
Factory botの基本的使い方
-
bin/rails g factory_bot:model user
コマンドでファイルの生成 -
生成されたファイルに下記のように記述することで、スペック全体でデフォルトデータのような形でファクトリを使用することができる。
-
共通部の切り出し、部品化のような形
1 FactoryBot.define do 2 factory :user do 3 first_name { "Aaron" } 4 last_name { "Sumner" } 5 email { "tester@example.com" } 6 password { "dottle-nouveau-pavilion-tights-furze" } 7 end 8 end
↓
-
切り出したFactoryを用いて、以下のような形でテストコードを簡潔な形で記述することができる。
1 require 'rails_helper' 2 3 describe User do 4 # 有効なファクトリを持つこと 5 it "has a valid factory" do 6 expect(FactoryBot.build(:user)).to be_valid 7 end 8 9 # 他のスペックが並ぶ ... 10 end
ファクトリ内の重複をなくす(trait)
-
traitとは
- FactoryBotで複数のサンプルデータを作成する際に使われるテクニック
- Factoryが複数ある場合、traitを使用することで、記述の重複を減らすことができ、可読性を高めたコードにすることできる。
FactoryBot.define do factory :モデル名 do trait :上記モデルを継承したインスタンスの属性値 do end end end
-
traitを使うまでの過程
- trait使用前の冗長なコード(重複箇所が多い)
FactoroyBot.define do factory :project do sequence(:name) { |n| "Test Project #{n}" } description "Sample project for testing purposes" due_on 1.week.from_now association :owner end # 昨日が締め切りのプロジェクト factroy :project_due_yesterday, class: Project do sequence(:name) { |n| "Test Project #{n}" } description "Sample project for testing purposes" due_on 1.day.ago association :owner end end
- 継承を使うことで、「class:Project」と重複した記述が不要になり、以下のコードになる。
FactoroyBot.define do factory :project do sequence(:name) { |n| "Test Project #{n}" } description "Sample project for testing purposes" due_on 1.week.from_now association :owner # 昨日が締め切りのプロジェクト factroy :project_due_yesterday do due_on 1.day.ago end end end
- 上記コードは親Factoryであるprojectを継承したproject_due_yesterdayが入れ子構造になっている。
factory :project factory :project_due_yestreday
- traitを使用することで、継承するモデル名を省略し、以下のようなコードを記述することができる。
FactoroyBot.define do factory :project do sequence(:name) { |n| "Test Project #{n}" } description "Sample project for testing purposes" due_on 1.week.from_now association :owner # 昨日が締め切り trait :due_yesterday do due_on 1.day.ago end end end
- specファイルでtraitを呼び出す場合は以下のような形で引数として渡す必要がある。
describe "latestatus" do # 締切日が過ぎていれば遅延していること it "is late when the due date is past today" do # 以下のように引数としてtraitを渡す必要がある project = FactoryBot.create(:project, :due_yesterday) expect(project).to be_late end end
4.コントローラスペック
- コントローラテストは簡単に追加することができるが、肥大化してしまうことがあるので注意
- 基本的にはアクセス制御が正しく動作しているかどうかを確認するテストに限定すること
- コントローラのテストは単体テストとして有効活用できるときに使うのにとどめて、使いすぎないようにすること
コントローラスペックの基本
- まず以下のコントローラを用意
1 class HomeController < ApplicationController
2
3 skip_before_action :authenticate_user!
4
5 def index
6 end
7 end
- 上記コントローラのRSpecファイルを以下のコマンドで生成
$ bin/rails g rspec:controller home --controller-specs --no-request-specs
↓(※rails7以降では自動的に生成される)
spec/controllers/home_controller_spec.rb
1 require 'rails_helper'
2
3 RSpec.describe HomeController, type: :controller do
4
5 end
- 基本テスト
1 require 'rails_helper'
2
3 RSpec.describe HomeController, type: :controller do
4 describe "#index" do
5 # 正常にレスポンスを返すこと
6 it "responds successfully" do
7 get :index
8 expect(response).to be_successful
9 end
10 end
11 end
認証が必要なコントローラスペック
- ログインユーザー認証が必要な場合は
spac/rails_helper.rb
にDiviseヘルパーモジュールを組み込む
1 RSpec.configure do |config|
2 # 設定ブロックの他の処理は省略 ...
3
4 # コントローラスペックでDeviseのテストヘルパーを使用する
5 config.include Devise::Test::ControllerHelpers, type: :controller
6 end
- 上記を記述することで、ログイン状態をシュミレートすることができる。
- Diviseのヘルパーメソッドをテストコードの中で使用することも可能になる。
5.システムスペック
- この章では、システム全体を一緒に使ってテストする
- 単体のmodelやcontrollerが他のmodelやcotrollerとを結合させて、システム全体として、うまく動作することを確認する。
- このようなテストを「システムスペック(受入テスト)」または「結合テスト」と呼ばれる。
システムスペックで使用するgem
- システムスペックでは、Copybaraを使用する。
- webフォームの入力が可能
- 画像の表示を検証することができる。
- リンクのクリックをすることが可能。
1 group :test do
2 gem 'capybara'
3
4 # その他のgemは省略 ...
5 end
システムスペックの基本
- Capybaraでは
click_link
やfill_in
、visit
といった理解しやすいメソッドが提供されていて、アプリケーションで必要な機能の シナリオ を書くことができる。 - 普段ブラウザで確認している動作をRSpecテスト用に言語化した感じ
1 require 'rails_helper'
2
3 RSpec.describe "Projects", type: :system do
4 before do
5 driven_by(:rack_test)
6 end
7
8 # ユーザーは新しいプロジェクトを作成する
9 scenario "user creates a new project" do
10 user = FactoryBot.create(:user)
11
12 visit root_path
13 click_link "Sign in"
14 fill_in "Email", with: user.email
15 fill_in "Password", with: user.password
16 click_button "Log in"
17
18 expect {
19 click_link "New Project"
20 fill_in "Name", with: "Test Project"
21 fill_in "Description", with: "Trying out Capybara"
22 click_button "Create Project"
23
24 expect(page).to have_content "Project was successfully created"
25 expect(page).to have_content "Test Project"
26 expect(page).to have_content "Owner: #{user.name}"
27 }.to change(user.projects, :count).by(1)
28 end
29 end
CopybaraのDSL
- DSL
- ある特定の目的を達成することだけに照準を絞った、独自の言語環境のこと
- ここではCopybaraを用いてシステムスペックを行う際に使用するメソッドのこと
- Copybaraのメソッド一覧
システムスペックをデバッグする
- テストが失敗する箇所に
save_and_open_page
を挟むことで、その直前で処理を止めて、ブラウザ上の動作を確認することができる。
1 scenario "guest adds a project" do
2 visit projects_path
3 save_and_open_page
4 click_link "New Project"
5 end
- 上記の場合、毎回手作業でブラウザ開く必要がある
- *
Launchy* gem
をインストールすれば自動的にブラウザが開くようになる。
1 group :test do
2 # Railsで元から追加されているgemは省略
3
4 gem 'launchy'
5 end
- デバッグが終わったタイミングで
save_and_open_page
の記述は削除する。
JavaScriptを使った操作テスト
- CopyBaraはRack::Testというドライバで動作しているが、JavaScriptの動作をサポートしていない。
-
driven_by
メソッドの記述はなくし、加えて、ここではjs: true
というオプション(タグ)を記述する。
1 require 'rails_helper'
2
3 RSpec.describe "Tasks", type: :system do
4 # ユーザーがタスクの状態を切り替える
5 scenario "user toggles a task", js: true do
6 user = FactoryBot.create(:user)
7 project = FactoryBot.create(:project,
8 name: "RSpec tutorial",
9 owner: user)
10 task = project.tasks.create!(name: "Finish RSpec tutorial")
--略
selenium-webdriver
のgemを使用する。- 使用するドライバは
driven_by
メソッドを使ってテストごとに変更することができ、これを共通設定(helperファイルに記述)とする。 spec/support/capybara.rb
RSpec.configure do |config|
config.before(:each, type: :system) do
driven_by :rack_test
end
# js: trueと記述されている場合はselenium_chromeドライバを使用することにする。
config.before(:each, type: :system, js: true) do
driven_by :selenium_chrome
end
end
- テスト実行中にブラウザを開かないようにするには
spec/support/capybara.rb
以下のように記述する。(ヘッドレスドライバ)
config.before(:each, type: :system, js: true) do
driven_by :selenium_chrome_headless
end
システムスペックとフィーチャースペック
- システムスペックはRails 5.1から導入されたシステムテストを利用している。
- それ以前はフィーチャスペック(feature specs)と呼ばれるRSpec Rails独自の機能を使って統合テストを書いていた。
- フィーチャースペックとシステムスペックの相違点
- spec/system ではなく spec/features にファイルを保存する
-
describe
メソッドではなくfeature
メソッドを使う -
type:
オプションに:system
ではなく:feature
を指定する
- 原則フィーチャスペックではなく、システムスペックを使うようにする。
- フィーチャスペックはレガシーな機能になりつつあるため、早めにシステムスペックに移行する方が良い。
テスト駆動開発において
- テスト駆動開発とは
- 最初にテストを書き、そのテストが動作する必要最低限の実装を行った後で、コードを洗練させていくこと。
- 別名テストファーストともいう
- テストコードを書く→失敗→エラー文を元にコーディング→、、、、でシステムを作り上げていく感じ
- テスト失敗のエラー文はどのようなコーディングをすればいいのかのヒントになる
- テスト駆動開発の信条はは「テストを前進させるために必要最小限のコードを書く
- 異常系のテスト
- 想定外の入力がなされた時に、それを考慮した出力結果がなされるかどうかのテスト
- ex)ユーザー登録機能
- 想定ではpasswordを4文字以上入力される形だったが、3文字しか入力されない場合にvalidationエラーが出力されるかどうか
- ex)ユーザー登録機能
- 想定外の入力がなされた時に、それを考慮した出力結果がなされるかどうかのテスト
難しかったこと
RSpec独自の記法
- RSpecはrailsのライブラリの一つですが、記法に関してはRSPec独特の記法のようなものがあり、こちら実際にコードを書きながら慣れていく日宇町があるなと思いました。また、メソッド等もたくさんあるので、こちらも実践を通じて都度使いこなせるようになっていきたいと感じました。
実務でRSpecを使用したことないと理解が難しい
- 本書はRSpecの実務で役立つ知識に焦点を当てて記述されており、自分のような実務経験が少ないと理解するのが難しいと感じました。基礎構文を丁寧に解説というよりかは、実務でどのようにコーディングするのかをサンプルコードを用いて記述されている印象を受けました。
なので、RSpecを1回も触ったことがない、Railsでの実務経験があまりないという方は、まずは下記にて基礎部分をザッと確認してから読み進めるのがいいかもしれません。
- また、一度で全てを完全に理解するのは難しいので、6割理解でまずは1度読み通して、実際にコーディングしていく中で立ち返りながら、徐々に理解を深めていければいいと感じました。
おわりに
ここまでEveryday Rails - RSpecによるRailsテスト入門の読了しての要約/感想記事を書かせていただきました。
Railsにおいてテストは開発と比べて、軽視されがちですが、質の高いシステムを作るのには欠かせないものとなります。
これを機にぜひとも、こちらの本書を通じて、RSpecの見識を深めるきっかかけになれば幸いです。
ここまで本記事を閲覧いただきありがとうございました!