『Everyday Rails - RSpecによるRailsテスト入門』を読んだので簡単にまとめてみました。
テストとは
そもそもテストとは何なのか。
つまり、テストはあなたに開発者としての自信を付けさせるものであるべきなのです。
テストはどうあるべきかということについては、\
- テストは信頼できるものであること
- テストは簡単に書けること
- テストは簡単に理解できること(今日も将来も)
と著者は言っています。今日も将来も簡単に理解できるというのは重要ですね。本文では非エンジニアでも読んでわかるという記載があります。モデルやそのメソッドやスキーマを見ずとも、テストを見ればその全容がパッとわかるととてもうれしいですね。じゃあ原理主義的に「簡単に理解できるもの」を書かなければいけないのか、簡単とは何か頭をひねって考えなければならないのかと言うと、
とはいえ結局、一番大事なことはテストが存在することです。
なるほど。
さあ、テストをつくってみよう
ジェネレータを使ってモデルのテストを作成する
$ bin/rails g rspec:model user
ジェネレータを使わないでじかに書いたらダメなの? と言う問いには、
必ずしもスペックファイルを作成するためにジェネレータを利用する必要はありません。しかし、ジェネレータの使用はタイプミスによるつまらないエラーを防止するための良い方法です。
まずはモデルのバリデーションテストをつくる
例えば、
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
バリデーションテストを一つずつ作る意味はあるのか? という疑問に著者が答えている部分がありました。
- バリデーションは書き忘れやすい
- テストを書いているときに追加しなければいけないバリデーションを思い出せる
「こんなテストは役に立たない。モデルに含まれるすべてのバリデーションを確認しようとしたらどれ くらい大変になるのかわかっているのか?」そんなふうに思っている人もいるかもしれません。ですが、 実際はあなたが考えている以上にバリデーションは書き忘れやすいものです。しかし、それよりもっと 大事なことは、テストを書いている 最中に モデルが持つべきバリデーションについて考えれば、バリデ
ーションの追加を忘れにくくなるということです。(このプロセスはテスト駆動開発でコードを書くのが 理想的ですし、最後は実際そうします。)
確かにそうですね。
バリデーションを書き換えたり、コメントアウトしてみたりする
一時的にバリデーションをコメントアウトしたり、テストを書き換えたりして、結果が変わることを確認してください。
nil
を入れたり数字しか入れられないところに文字列を入れたりしてみます。
次はメソッドをテストする
it "returns a user's full name as a string." do
user = User.new(
first_name: "John",
last_name: "Doe",
email: "johndoe@example.com",
)
expect(user.name).to eq "John Doe"
end
rspecでは==
はeq
を使います。
正常系だけでなく異常系もテストします。
マッチャの色々
be_valid
eq
include
be_empty
他にも色々あります。
https://github.com/rspec/rspec-expectations
DRY にする
describe
でメソッドごとにまとめます。
RSpec.describe Note, type: :model do
describe "search message for a term" do
...
end
end
context
で場合分けをします。値が返ってくる場合と返ってこない場合などです。
RSpec.describe Note, type: :model do
describe "search message for a term" do
context "when a match is found" do
...
end
context "when no match is found" do
...
end
end
end
before
で重複データをまとめてセットアップします。インスタンス変数にアサインするので@
をつけるのを忘れないようにしましょう。
before do
@user = User.create(
first_name: "Joe",
last_name: "Tester",
email: "joetester@example.com",
password: "password",
)
@project = user.projects.create(
name: "Test Project",
)
end
じゃあどのくらいまでDRYにすればいいのか?
こうなった場合は重複を検討してください。
- 大きなスペックファイルを頻繁にスクロールしている
- 外部のサポートファイルを大量に読み込んでいる
もし自分がテストしている内容を確認するために、大きなスペックファイルを頻繁にスクロールしているようなら(もしくはあとで説明する外部のサポートファイルを大量に読 み込んでいるようなら)、テストデータのセットアップを小さな describe ブロックの中で重複させること を検討してください。
また、note1
をnote_with_numbers_only
とするように、変数名によってわかりやすさを向上させる方法も良いです。
テストの場合は DRY であることよりも読みやすいことの方が重要です。
気をつけること
- 期待する結果は能動形で明示的に記述すること
- 起きてほしいことと、起きてほしくないことをテストすること
- 境界値テストすること
- 可読性を上げるためにスペックを整理すること
FactoryBotとは
FactoryBot という gem はテストをきれいで読みやすくリアルにしてくれます。でも多用しすぎると遅くなるので気をつけましょう。
ジェネレータで Factory を追加する
bin/rails g factory_bot:model user
Factory を追加する
FactoryBot.define do
factory :user do
first_name { "Aaron" }
last_name { "Sumner" }
email { "tester@example.com" }
password { "dottle-nouveau-pavilion-tights-furze" }
end
end
セットアップがちゃんと完了しているか確認するテストを書きましょう。
it "has a valid factory" do
expect(FactoryBot.build(:user)).to be_valid
end
それからuser
をセットアップしていた部分を Factory に変えます。デフォルト部分を置き換えるなら、
user = FactoryBot.build(:user, first_name: nil)
便利な機能を色々使ってみる
- ユーザーが二つ必要な場合などは連番をつける
シーケンス
を使いましょう。 -
関連
を扱うこともできます。 -
継承
やtrait
でDRYにもできます。 -
コールバック
によってcreate
後のアクションも指定できます。after(:create) { |project| create_list(:note, 5, project: project) }
コントローラーのテストは?
コントローラーのテストには不十分な点があります。例えば destroy アクションのテストを作ったものの、結局UIが用意されていなかったなどです。使うとしたら場面を限定するようにしましょう。読めるようにはしておいた方が良いです。
フィーチャースペックを使ってみる
フィーチャースペックとは
フィーチャースペックはモデルとコントローラーが他のモデルやコントローラーとうまく一緒に動作することを確認するものです。またこれは受入テストや統合テストとも呼ばれます。
コントローラースペックと何が違うのか
Capybara
の機能であるvisit
、click_link
、click_button
などによってユーザーの実際のUI操作を再現できます。expect
部分ではhave_content
を使うことで実際に画面に表示されているコンテンツを確認します。これによって複数のモデルやコントローラーの動作をテストできます。
Capybara
のDSL
機能を使えばファイルのアップロードやCSS
もテストすることができます。
https://github.com/teamcapybara/capybara#the-dsl
どこでテストが失敗したかわからないときは
Capybara
はUIを持たないブラウザで画面を操作します。なので処理を一つ一つたどることができません。そういったときはsave_and_open_page
を挟み込んでみましょう。binding_pry
のような感じですね。
scenario "guest adds a project" do
visit projects_path
save_and_open_page
click_link "New Project"
end
JavaScriptを使ったテスト
js: true
でJSを使うテストが簡単に作れます。実際にテストするとブラウザが立ち上がって自動入力してくれます。手動よりは早いですがやはり通常のテストよりかなり遅くなるのでJSのテストは必要な時にだけ有効にするのが良いでしょう。
また継続的インテグレーション(CI)環境では:selenium_chrome_headless
をドライバーに設定してウィンドウを開かないでテストすることができます。
シナリオ内にusing_wait_time(15)
を入れることで、JSの読み込みの待ち時間を調整することもできます。
リクエストスペックを使ってAPIをテストしてみる
リクエストスペックはCapybara
を使いません。プログラム同士の対話には必要ないからです。コントローラースペックと似ていますが、ログインを入れたり直接パスを使えたりといったより高いレベルのテストができます。
スペックをDRYに保つには
サポートモジュールに切り出す
ログイン方法や文言が変わった場合に全ての部分を変更するのは大変です。例えばログイン作業をヘルパーメソッドに切り出すことができます。フィーチャースペックでよく使います。
module LoginSupport
def sign_in_as(user)
visit root_path
click_link "Sign in"
fill_in "Email", with: user.email
fill_in "Password", with: user.password
click_button "Log in"
end
end
RSpec.configure do |config|
config.include LoginSupport
end
モジュール内のメソッド名は、コードを読んだときに目的がぱっとわかるような名前にしてください。もしメソッドの処理を理解するために、いちいちファイルを切り替える必要があるのなら、それはかえってテストを不便にしてしまっています。
名前はパッとわかるようにしましょう。
letを使う
before
を使ってログイン処理などを行なうのも悪くありません。ですがこれは毎回実行されてしまいます。予期せぬ影響があったりテストが遅くなったりします。
let
は呼ばれた時に初めてデータを読み込むという遅延読み込みをしてくれます。使う時点で作ってくれるのでbefore
の時のようにインスタンス変数に入れる必要もありません。
RSpec.describe Task, type: :model do
let(:project) { FactoryBot.create(:project) }
it "is valid with a project and name" do
task = Task.new(
project: project,
name: "Test task"
)
expect(task).to be_valid
end
end
遅延読み込みでなく通常通りに先に読み込む場合はlet!
を使います。ですが!
は読み落としやすいです。なので使う場合はbefore
にしたほうがわかりやすいかどうかを再検討してみてください。
shared_contextに切り出す
セットアップ作業などを切り出すこともできます。
RSpec.shared_context "project setup" do
let(:user) { FactoryBot.create(:user) }
let(:project) { FactoryBot.create(:project, owner: user) }
let(:task) { project.tasks.create!(name: "Test task") }
end
マッチャをカスタムしてさらに読みやすくする
RSpec の重要な信条のひとつは、人間にとっての読み やすさです。ですので、カスタムマッチャを使って読みやすさを改善してみましょう。
RSpec::Matchers.define :have_content_type do |expected|
match do |actual|
content_types = {
html: "text/html",
json: "application/json",
}
actual.content_type == content_types[expected.to_sym]
end
end
expect(response.content_type).to eq "application/json"
がexpect(response).to have_content_type :json
になりました。
エラーメッセージやエイリアスも作ることができます。
カスタムマッチャ作りにハマっていく前に、shoulda-matchers gem も一度見ておいてください。 この gem はテストをきれいにする便利なマッチャをたくさん提供してくれます。
二つのテストを一つに集約する
aggregate_failures
を使って集約することができます。
it "responds successfully" do
sign_in @user
get :index
aggregate_failures do
expect(response).to be_success
expect(response).to have_http_status "200"
end
end
メソッドに切り出して読みやすくする
これは「シングルレベルの抽象化を施したテスト(testing at a single level of abstraction)」として知られるテクニックです。
scenario "user toggles a task", js: true do
go_to_project "RSpec tutorial"
end
def go_to_project(name)
visit root_path
click_link name
end
もしかするとプログラミングを知らない人がこのテストを読んでも、何をやっているのかある程度理解できるかもしれません。
速いテストをより素早く書くには
実行速度の速いスペックをより素早く書くにはどうすればいいでしょうか。
- 構文を簡単かつきれいにしてスペックをより短くする
-
Shoulda Matchers
を使えば1行に短縮できる - テストファーストで書く時に簡単
-
it { is_expected.to validate_presence_of :first_name }
- お気に入りエディタを活用してキー入力を減らす
- モックとスタブでボトルネックを切り離す
- モックはデータベースにアクセスしない代役
- スタブは呼び出されるとテスト用に本物の結果を返すダミーメソッド
- 遅いスペックを除外するタグを使う
-
focus: true
を使って実行するスペックを限定する - 実行時に
~
をつければタグ以外の全テストを実行できる - タグはコミット前に忘れず削除する
- 日常的に必要ない場合は
skip
を使う
-
- テスト全体をスピードアップさせるテクニック
- テストを並列に実行する
その他のテスト
アップロード機能のテスト
attach_file
メソッドを使います。アップロードするファイルは忘れずにバージョン管理システムにコミットしましょう。
バックグラウンドワーカーのテスト
queue_adapter
をセットします。
before do
ActiveJob::Base.queue_adapter = :test
end
ブロックスタイルのexpect
と組み合わせてテストします。
expect {
GeocodeUserJob.perform_later(user)
}.to have_enqueued_job.with(user)
メール送信のテスト
二つのレベルでテストします。書き方は他のテストとそんなに変わりません。
- メールが正しく作られているか
- 正しい宛先に送られているか
送信されたmail
を取得してフィーチャースペックでテストすることもできます。
mail = ActionMailer::Base.deliveries.last
メーラースペック、フィーチャースペック、モデルスペックのどこでどれだけテストをするかは開発者次第です。選択肢がいろいろあります。
外部のAPIを叩くテスト
外部のAPIを叩く場合気をつける点があります。
- そのテストだけ遅くなる
- レートリミットを超えたとたんエラーが発生する
VCRgem
とWebMock
を使うことで回避できます。WebMock
はHTTPをスタブ化してくれるライブラリです。
gem 'vcr'
gem 'webmock'
設定ファイルで設定してから、テストにvcr: true
にすることで有効化できます。レスポンスの内容はspec/cassettes
に保存されます。
私は VCR が大好きですが、VCR には短所もあります。特に、カセットが古びてしまう問題には注意が必要です。……もしテストに使っている外部 API の仕様が変わってしまっても、あなたはカセットが古くなっていることを知る術がない、ということです。
これについての対処法としては、
- バージョン管理にカセットファイルを含めない
- 一定頻度でカセットを再記録する
二つ目の注意点として、API のシークレットトークンやユーザーの個人情報といった機密情報 をカセットに含めないようにしてください。
読後のまとめ
RSpecによるテストの方法が網羅的かつ実戦形式で記載されていたので理解しやすかったです。頭から最後まで写経しながら通読することでRSpecで行なうテストの全体像がしっかり頭に入るようになっています。とても親しみやすい文体で書かれていて、途中で頭をひねようなこともありません。いい本でした。