はじめに
Rails初学者の定番教材であるRailsチュートリアルを完走しました。
完走後、Railsチュートリアルの読み物ガイドに目を通してみて、
次のステップとして実践的なテストフレームワークであるRSpecについて学ぶことにしました。
そして、Everyday Railsを購入し、基本的なスペックが一通り書けるようになるまで進めました。
教材を通読・サンプルコードを書いた程度だとまだ理解が浅いので、
実践を通じてRSpecの使い方を理解するためにRailsチュートリアルのテストをMinitestからRSpecに置き換えてみることにしました。
読み物ガイドで紹介されていた下の記事を答え合わせ的に使っていきます。
意識すること
- 過度にDRYであることよりも可読性を重視する。
- ただ、ファクトリの扱いには慣れたいのでテストデータの用意はなるべくファクトリで。
- Railsチュートリアルに縛られすぎない。
- 例えば「Railsチュートリアルがコントローラテストだったからコントローラスペックを書く」のような考え方は避ける。
- まず動かし、次に正しくし、それから速くする(Make it work, make it right, make it fast)」を意識する
- Everyday Railsで紹介されていた格言です。
- 細かいところや便利なマッチャを使うことにこだわりすぎず、サクッとテストを書いてみる。
環境
Railsチュートリアルの環境をそのまま利用します。
- Ruby 2.7.4
- Ruby on Rails 6.0.4
追加でインストールするgem
- rspec-rails 5.1.1
- FactoryBot 6.2.0
- capybara 3.28.0
- selenium-webdriver 3.142.4
- webdrivers 4.1.2
リンク
- RailsチュートリアルのテストをRSpecに置き換えてみる【セットアップ~5章】 - Qiita 本記事
- RailsチュートリアルのテストをRSpecに置き換えてみる【6章~9章】 - Qiita
- RailsチュートリアルのテストをRSpecに置き換えてみる【10章~12章】 - Qiita
- RailsチュートリアルのテストをRSpecに置き換えてみる【13章~14章+RuboCopの導入】 - Qiita
セットアップ
必要なgemのインストール
まずはGemfile
を編集して必要なgemを使えるようにします。
Railsのバージョンにもよりますが、
システムスペック用のcapybara、selenium-webdriver、webdribersはデフォルトでインストールされています。
group :development, :test do
# RSpec
gem 'rspec-rails'
# テストデータの作成
gem 'factory_bot_rails'
end
group :test do
gem 'capybara', '3.28.0'
gem 'selenium-webdriver', '3.142.4'
gem 'webdrivers', '4.1.2'
# 省略...
end
その他便利なgem
- 便利なマッチャが使える
- shoulda-matchers
- テストの実行を並列に
- parallel_tests
- システムスペックのデバッグを便利に
- launchy
Gemfile
の編集が終わったら bundle install
を実行します。
bundle install
テストデータベースの設定
テスト実行時に接続するテスト用のデータベースを設定します。
# spliteの場合
test:
<<: *default
database: db/test.sqlite3
# MySQLやPostgreSQLを使っている場合
test:
<<: *default
database: projects_test
RSpecの設定
まず、次のコマンドを実行します。
$ bin/rails generate rspec:install
# 結果
create .rspec
create spec
create spec/spec_helper.rb
create spec/rails_helper.rb
以下のファイルとディレクトリが生成されます。
-
.rspec
- RSpec用設定ファイル
-
spec
- スペックファイルを格納するディレクトリ
spec_helper.rb
-
rails_helper.rb
- ヘルパーファイル
この時点でRSpecが動くようになります。
出力を読みやすく
/.rspec
に --format documentation
を追加して出力を読みやすくします。
--require spec_helper
--format documentation
binstub
を使って短いコマンドで実行出来るようにする
RSpecを実行する際は bundle exec rspec
のように毎回 bundle exec
を付ける必要があります。
これは、実行ファイルがプロジェクトのコンテキストで実行されるようにするためです。
詳細 : bundle execとbinstubを理解する - Qiita
ただ、毎回 bundle exec
を付けるのは面倒です。
binstubs
コマンドを使うと少しだけコマンドを短くすることが出来ます。
次のコードを実行すると bin/rspec
でテストを走らせることが出来るようになります。
bundle binstubs rspec-core
また、この他にも独自のエイリアスを設定してコマンドを短くすることも出来ます。
ジェネレータの設定
rails genarate
を実行した時に、スペックファイルも一緒に生成されるようにします。
また、使用頻度の低いスペックファイルが生成されないようにします。
require_relative"boot" require"rails/all"
# 省略 ...
Bundler.require(*Rails.groups)
module Projects
class Application < Rails::Application
# 省略 ...
config.generators do |g|
g.test_framework :rspec,
fixtures: false,
view_specs: false,
helper_specs: false,
routing_specs: false
end
end
end
-
fixture
- フィクスチャを作成しない。
-
view_specs
- ビュースペックを作成するしない。
-
helper_specs
- ヘルパーファイル用のスペックを作成しない。
-
routing_specs
-
routes.rb
用のスペックを作成しない。 - アプリケーションが大規模になってきたら導入すると良いらしい。
-
ちなみに、自動生成しないというだけであって、
- 手動で追加
- 自動生成されたファイルを削除すること
は問題ありません。
3章 ほぼ静的なページの作成
RSpecで書き換える
まずは rails g
コマンドを叩いてスペックファイルを生成します。
bin/rails g rspec:request static_pages
Railsチュートリアルではコントローラ用のテストを書いていました。
しかし、以下のような理由からリクエストスペックとして実装していくことにしました。
- 現在コントローラスペックは公式に非推奨で、リクエストスペック/システムスペックへの移行が推奨されている。
- テストしたいのは「適切なレスポンスが返ってくる」こと。
3.14
require 'rails_helper'
RSpec.describe 'StaticPages', type: :request do
describe '#home' do
it "responds successfully" do
get static_pages_home_path
expect(response).to have_http_status "200"
end
end
describe '#help' do
it "responds successfully" do
get static_pages_help_path
expect(response).to have_http_status "200"
end
end
describe '#about' do
it "responds successfully" do
get static_pages_about_path
expect(response).to have_http_status "200"
end
end
end
最初なので少し詳しく。
-
describe
によりテストをグループ化します。- 「describe」は「〜を記述する」「〜を説明する」などの意味です。
- ここでは「各アクションに対応するテストを書く」ということを説明/記述しています。
- 原則として、example(
it
から始まる部分 )1つに付き1つの結果を期待します。- このようにしておくと、テストが失敗した時に原因を特定しやすくなります。
-
expect
から始まる部分はエクスペクテーションと言い、ここで実際にテストを実行します。- テストする値を
expect
メソッドに渡し、それに続けてマッチャを呼び出します。 - 「expect」は「〜を期待する」という意味です。
- ここでは「ステータスコード200のレスポンスを期待する」という意味になります。
- テストする値を
-
have_http_status
というマッチャを使ってレスポンスが正常に返ってきていることを確認しています。
テストを走らせてみます。
$ bin/rspec
# 省略...
StaticPages
#home
responds successfully
#help
responds successfully
#about
responds successfully
OKですね。
3.16
about
ページのテストを追加するだけなので割愛。
3.25
RSpec.describe 'StaticPages', type: :request do
let(:base_title) { 'Ruby on Rails Tutorial Sample App' }
describe '#home' do
.
.
it "have a correct title" do
get static_pages_home_path
expect(response.body).to include "Home | #{base_title}"
end
end
describe '#help' do
.
.
it "have a correct title" do
get static_pages_help_path
expect(response.body).to include "Help | #{base_title}"
end
end
describe '#about' do
.
.
it "have a correct title" do
get static_pages_about_path
expect(response.body).to include "About | #{base_title}"
end
end
end
let
を利用してデータを遅延読み込みします。
タイトルの確認には response.body
と include
マッチャを使いました。
3.4.3演習
contact
ページのテストを追加するだけなので割愛。
3.4.4演習
名前付きルート root_path
を使うように変更します。
describe "root" do
it "responds successfully" do
get root_path
expect(response).to have_http_status "200"
end
答え合わせ
コード例〜第3章〜|RailsチュートリアルのテストをRSpecで書き換える
「リクエストスペックを使う」という点に関しては同じでした。
相違点について触れていきます。
ステータスコードをシンボルに置き換え
have_http_status "200"
と記述していましたが、シンボルに置き換え出来るようです。
have_http_status
というマッチャを使用してHTTPステータスコードが:ok (200)
であるかを検証しています。require 'rails_helper' RSpec.describe 'StaticPages', type: :request do describe '#home' do it '正常にレスポンスを返すこと' do get static_pages_home_path expect(response).to have_http_status :ok end end # 省略...
このように、ステータスコードはシンボルに置き換えて記述することができます。
紹介されていた参考記事 : 【Rails】APIモードで使えるHTTPステータスコードのシンボルまとめ - Qiita
シンボルの方がわかりやすいので今後はなるべくシンボルを使っていきます。
example
を表す文字列が日本語
it
に渡す文字列が日本語です。
Everyday Railsでは全て英語だったので、そういうものかと思っていました。
使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」 - Qiita
RSpec 導入時にチーム内で意識・決定しておきたいルール
↑の記事を見る限り、
- チームメンバー全員英語の読み書きに不自由しない(=頑張って英語を読み書きするためのコストが発生しない)なら英語が理想。
- そうでないなら日本語でもOK。
- チームで合わせることが大事。
という感じでしょうか。
どちらがスタンダードなのか知りたいな・・・
ひとまず英語で進めようと思います。
4章 Rails風味のRuby
RSpecで書き換える
4.4
describe "root" do
it "have a correct title" do
get root_path
expect(response.body).to include "<title>#{base_title}</title>"
end
end
答え合わせ
コード例〜第4章〜|RailsチュートリアルのテストをRSpecで書き換える
一緒でした。
5章 レイアウトを作成する
RSpecで書き換える
5.21
3.4.3で実施したので割愛
5.28
RSpec.describe "StaticPages", type: :request do
.
.
describe "#help" do
it "responds successfully" do
# 定義した名前付きルートを使用する
get help_path
expect(response).to have_http_status :ok
end
it "have a correct title" do
get help_path
expect(response.body).to include "Help | #{base_title}"
end
end
# 省略...
# 他も同様に名前付きルートを使用するように変更
routes.rb
で定義した名前付きルートが正しく動くかテストします。
5.32
リンクが正しく表示されていることを検証します。Railsチュートリアルではインテグレーションテストを書いていました。
やりたいことはUIの検証なので、システムスペックを使っていきます。
RSpec.describe "StaticPages", type: :system do
before do
# jsドライバは不要なのでRack::Testを使用
driven_by(:rack_test)
end
describe "root" do
it "have two links to root path and one link to help, about, contact" do
visit root_path
# root_pathを2つ以上含む
expect(page.all("a[href=\"#{root_path}\"]").length).to eq 2
# help, about, contactパスを含む
expect(page).to have_link "Help" , href: help_path
expect(page).to have_link "About" , href: about_path
expect(page).to have_link "Contact" , href: contact_path
end
end
end
JavaScript実行可能なドライバは不要なので、高速なRack::Testドライバを使います。
have_link
でリンクを確認、複数リンクは all
を使って取得・カウントします。
all
メソッド参考 : Method: Capybara::Node::Finders#all — Documentation for jnicklas/capybara (master)
5.37
full_title
というヘルパーメソッドをRSpecで使えるようにします。
RSpec.configure do |config|
# 省略...
# 追記
include ApplicationHelper
end
full_title
を導入します。
require 'rails_helper'
RSpec.describe ApplicationHelper, type: :helper do
describe "full_title" do
let(:base_title) { 'Ruby on Rails Tutorial Sample App' }
context "argment isn't an empty string" do
it "returns argment string + base title" do
expect(full_title("Page Title")).to eq "Page Title | #{base_title}"
end
end
context "argment is an empty string" do
it "returns baset title" do
expect(full_title).to eq "#{base_title}"
end
end
end
end
5.44
require 'rails_helper'
RSpec.describe "Users", type: :request do
describe "#new" do
it "responds successfully" do
get signup_path
expect(response).to have_http_status :ok
end
end
end
signup_path
が正しくレスポンスを返すことをテストします。
5.4.2 演習
RSpec.describe "Users", type: :request do
describe "#new" do
# 省略...
it "have a correct title" do
get signup_path
expect(response.body).to include full_title("Sign up")
end
end
end
signup_path
が正しいタイトルを持っているかテストします。
答え合わせ
コード例〜第5章〜|RailsチュートリアルのテストをRSpecで書き換える
UIのテストにシステムスペックを使う点など、ほぼ一緒でした。
5.32
# 自分が書いたテスト
expect(page.all("a[href=\"#{root_path}\"]").length).to eq 2
# コード例のテスト
link_to_root = page.find_all("a[href=\"#{root_path}\"]")
expect(link_to_root.size).to eq 2
all
ではなく find_all
を使っている点、返り値を引数に入れている点が異なります。
#all([kind = Capybara.default_selector], locator = nil, **options)
⇒ Capybara::Result
#all([kind = Capybara.default_selector], locator = nil, **options) {|element| ... } ⇒ Capybara::Result
Also known as: find_allMethod: Capybara::Node::Finders#all — Documentation for jnicklas/capybara (master)
公式の記述によると、all
と find_all
は同じメソッドのようです。
返り値を引数に入れる、find_all
を使用する方がわかりやすいので変更します。
describe "root" do
it "have two links to root path and one link to help, about, contact" do
visit root_path
links_to_root = page.find_all("a[href=\"#{root_path}\"]")
expect(links_to_root.length).to eq 2
expect(page).to have_link "Help" , href: help_path
expect(page).to have_link "About" , href: about_path
expect(page).to have_link "Contact" , href: contact_path
end
end
まとめ
static_pages_controller
のテストを見比べてみます。
Minitestのコード(コントローラの単体テスト)
require 'test_helper'
class StaticPagesControllerTest < ActionDispatch::IntegrationTest
test "should get home" do
get root_path
assert_response :success
assert_select "title", "Ruby on Rails Tutorial Sample App"
end
test "should get help" do
get help_path
assert_response :success
assert_select "title", "Help | Ruby on Rails Tutorial Sample App"
end
test "should get about" do
get about_path
assert_response :success
assert_select "title", "About | Ruby on Rails Tutorial Sample App"
end
test "should get contact" do
get contact_path
assert_response :success
assert_select "title", "Contact | Ruby on Rails Tutorial Sample App"
end
end
RSpecのコード(リクエストスペック)
require 'rails_helper'
RSpec.describe "StaticPages", type: :request do
let(:base_title) { "Ruby on Rails Tutorial Sample App" }
describe "root" do
it "responds successfully" do
get root_path
expect(response).to have_http_status :ok
end
it "have a correct title" do
get root_path
expect(response.body).to include "#{base_title}"
end
end
describe "#help" do
it "responds successfully" do
get help_path
expect(response).to have_http_status :ok
end
it "have a correct title" do
get help_path
expect(response.body).to include "Help | #{base_title}"
end
end
describe "#about" do
it "responds successfully" do
get about_path
expect(response).to have_http_status :ok
end
it "have a correct title" do
get about_path
expect(response.body).to include "About | #{base_title}"
end
end
describe "contact" do
it "responds successfully" do
get contact_path
expect(response).to have_http_status :ok
end
it "have a correct title" do
get contact_path
expect(response.body).to include "Contact | #{base_title}"
end
end
end
正直、現時点のような単純なテストではMinitestの方が少ないコード量でスッキリ書けますね。
今後よりテストが複雑になってくるとRSpecの方が可読性・メンテナンス性などの点で勝るとは思いますが。
Railsチュートリアルでも言及がありましたが、今書いているようなUIのテストはすぐに壊れてしまいそうです。
実際の開発ではクラスやIDを上手く使ってテストしやすいUIを書くようにしたいです。
参考記事
答え合わせ用に使用
RailsチュートリアルのテストをRSpecで書き換える
RSpecのREADME.md
https://github.com/rspec/rspec-rails
システムスペック/フィーチャスペック/リクエストスペックの違い
System specs, feature specs, request specs–what’s the difference?
CapybaraのREADME.md
capybara/README.md at master · teamcapybara/capybara
使えるRSpec入門
「Everyday Rails」の翻訳者である@jnchitoさんの書いた記事です。
- 使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」 - Qiita
-
使えるRSpec入門・その2「使用頻度の高いマッチャを使いこなす」 - Qiita
- 一通り目を通したい。
-
使えるRSpec入門・その3「ゼロからわかるモック(mock)を使ったテストの書き方」 - Qiita
- モックをあまり理解出来ていないのでそのうち読む。
-
使えるRSpec入門・その4「どんなブラウザ操作も自由自在!逆引きCapybara大辞典」 - Qiita
- 逆引き的に使う。