RSpecとは
Rubyプログラムやアプリケーションを自動的にテストするための振る舞い駆動型開発(BDD)フレームワーク。
BDDとは
開発の前に仕様や要件を定義し、それを基にテストを開発者以外がみても理解できるように自然言語に近い形式で記述する。
テストを書く意味
- アプリケーションの品質担保
- システム要件の理解度向上
- プログラムのバグ削減
- 不具合再発防止
- 後々リファクタリングしやすくするため。
「テストコードがない状態だとリファクタリングできない。きれいなコードを書くなら、まずテストコードから」
テストを書くときに意識すること
- DRYであること(Don't Repeat Yourself:同じことを繰り返すな)
- 信頼できるものであること
- 誰が見ても理解できること。将来の人のためにも。
- テストコードのコーディングルールを厳しくしすぎない。
モデルスペックテスト
validationテスト
RSpec.describe User, type: model do
describe 'validation' do
#正常系
it "is valid when firstname, lastname, email, and password are present" do
user = User.new(
firstname: 'John',
lastname: 'Williams',
email: 'test@example.com',
password: 'D3keL1fS'
)
expect(user).to be_valid
end
#異常系
it "is invalid when firstname is missing" do
user = User.new(
firstname: nil,
lastname: "Williams",
email: "test@example.com",
password: 'D3keL1fS'
)
user.valid?
expect(user.errors[:firstname]).to include("can't be blank")
end
end
end
-
describe
で「この機能のテストをするよ。」とテストの構造をグルーピングしている。このケースではvalidationをチェックするもの。 -
it
はテストをexample( it で始まる1行)単位にまとめている。itの後には期待する内容を記述する。このケースでは"firstname, lastnaem, email, passwordがあるとき
とfirstnameがないとき
の2つのテストを記述。 -
expect
は期待する結果。- 正常系は
expect(contact).to be_valid
はuser
がbe_valid
の時。つまりvalid(値が存在している)を期待する。 - 異常系は
valid?
でfalseだったらエラーメッセージが"can't be blank"
という文字列を含んでいることを期待する。
- 正常系は
以下の設定がされている場合、firstnameが空白(nilや空の文字列)であると"can't be blank"というエラーメッセージがuser.errors[:firstname]
に追加される。
class User < ApplicationRecord
validates :firstname, presence: true
# 他のバリデーションや設定
end
上のテストコードをリファクタリングする
beforeで共通化
DRY原則にしたがってit内で同じコードがあるので共通化をする。重複しているUseオブジェクト生成を一つにまとめてこれをインスタンス変数として定義する。
user = User.new(
firstname: "John",
lastname: "Williams"
email: "test@example.com",
password: 'D3keL1fS'
)
contextでまとめる
各ブロックで共通している処理があればbeforeで書くのが推奨される。機能をグループ化する点ではdescribe
もcontext
も同じだが、context
は条件を分けるときにつかう。より整理されコードが見やすくなる。
RSpec.describe User, type: :model do
describe 'validation' do
before :each do
@user = User.new(
firstname: "John",
lastname: "Williams",
email: "test@example.com",
password: 'D3keL1fS'
)
context 'when firstname, lastname, email, and password are present' do
it 'is valid' do
expect(@user).to be_valid
end
end
context 'when firstname is missing' do
it 'is invalid' do
@user.firstname = nil
@user.valid?
expect(@user.errors[:firstname]).to include("can't be blank")
end
end
end
end
describe
は親、context
は子。つまり親の中でbeforeで定義した@user
は子のcontext内で使える。
beforeとlet
before
ブロックを使うと describe
や context
ブロックの内部で、各テストの実行前に共通のインスタンス変数をセットアップできる。しかし問題点が二つある。
1. before の中に書いたコードは describe や context の内部に書いたテストを実行するたびに 毎回 実行される。これはテストに予期しない影響を及ぼす恐れがある。使う必要のないデータを作成してテストを遅くする原因となる。
2.要件が増えるにつれてテストの可読性を悪くする。
こうした問題に対処するため、RSpecは let
というメソッドを提供している。let
は遅延評価であり、必要になった時に使われる。
RSpec.describe User, type: :model do
describe 'validation' do
let(:user) do
User.new(
firstname: "John",
lastname: "Williams",
email: "test@example.com",
password: 'D3keL1fS'
)
end
context 'when firstname, lastname, email, and password are present' do
it 'is valid' do
expect(user).to be_valid
end
end
context 'when firstname is missing' do
it 'is invalid' do
user.firstname = nil
user.valid?
expect(user.errors[:firstname]).to include("can't be blank")
end
end
end
end
before
よりlet
の方が可読性がよくなる。
Factory Botでテストデータを作成
RSpecと組み合わせて使われることが多いテストデータの生成ライブラリ。Factory Botを使用することで、テストケースごとに必要なデータを「工場(Factory)」と呼ばれるテンプレートから簡単に生成できる。
ファクトリの定義
FactoryBot.define do
factory :user do
firstname { "John" }
lastname { "Williams" }
email { "test@example.com" }
password { 'D3keL1fS' }
end
end
テスト内での使用
FactoryBot.build(:user)
と書くだけで、簡単にインスタンス化できる。テストデータを作れば、そのユーザーの名前は毎回基本的に John Williams になる。メールアドレスもパスワードも最初から設定された状態になる。
RSpec.describe User, type: :model do
let(:user) { FactoryBot.build(:user) }
it "is valid with a firstname, lastname, email, and password " do
expect(user).to be_valid
end
it "is not valid without a firstname" do
user.firstname = nil
user.valid?
expect(user.errors[:firstname]).to include("can't be blank")
end
end
validationテストはするべきか
必ずしもこのような単純なvalidationの時はテストの量が膨大になるので書く必要はない。
validates :firstname, presence: true
しかし、複雑な正規表現、カスタムバリデータ、ややこしい条件、お金が発生する重要な場面ではテストは必要だと考える。以下はemailのフォーマットをチェックしている。
validates :email, presence: true, format: { with: /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i/ }
System Spec
Capybaraでシュミレーションテスト
ページの読み込み、フォームの入力・送信、リンクやボタンのクリックなど アプリケーションを実際に動かしているかのように動作テストができる。
Capybara DSLのドキュメントにはそれ以外にも便利なメソッドがある。
メソッド | 操作 |
---|---|
visit | 指定したページに遷移 |
click_link | 指定したリンク文字列を持つaタグをクリック |
fill_in | 入力したい文字列を指定のフォームに入力 |
click_button | 指定したラベルを持つボタンをクリック |
ログインし、プロジェクト作成するテスト
require 'rails_helper'
RSpec.describe "Projects", type: :system do
before do
driven_by(:rack_test)
end
scenario "user creates a new project" do #itと同じexampleの起点
#最初に新しいテストユーザーを作成
user = FactoryBot.create(:user)
visit root_path
#ログイン画面から作成したユーザーでログイン
click_link "Sign in"
#フォームにemailとpasswordを入力
fill_in "Email", with: user.email
fill_in "Password", with: user.password
#ログインボタンをクリックしてログイン
click_button "Log in"
#ユーザーは新しいプロジェクトを作成する
expect {
#新しくプロジェクト作成する
click_link "New Project"
#フォームにプロジェクト名と説明文を入力
fill_in "Name", with: "Test Project"
fill_in "Description", with: "Trying out Capybara"
click_button "Create Project"
expect(page).to have_content "Project was successfully created"
expect(page).to have_content "Test Project"
#プロジェクトのオーナー名を作成したユーザー名と保有するプロジェクトが+1したものを表示されているか確認
expect(page).to have_content "Owner: #{user.name}"
}.to change(user.projects, :count).by(1)
end
-
expect{}
の中ではブラウザ上でテストしたいステップを明示的に記述し、それから、結果の表示が期待どおりになっていることを検証している。 -
have_content
で特定のテキストがページ上に表示されているか確認できる。
テスト駆動開発(TDD)。レッドからグリーンへ
10章までで基礎知識を勉強したあと、11章でテスト駆動開発を行う。スペックを実行してエラー文を確認し、コードを書き加えて赤(エラー状態)」から「緑(成功状態)」に遷移するプロセスを体験できる。
参考書籍