1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

RailsチュートリアルのテストをRSpecに置き換えてみる【セットアップ~5章】

Last updated at Posted at 2022-03-18

はじめに

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

リンク

セットアップ

必要な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 を追加して出力を読みやすくします。

.rspec
--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

spec/requests/static_pages_spec.rb
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」は「〜を記述する」「〜を説明する」などの意味です。
    • ここでは「各アクションに対応するテストを書く」ということを説明/記述しています。
  • 原則として、exampleit から始まる部分 )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

spec/requests/static_pages_spec.rb
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.bodyinclude マッチャを使いました。

3.4.3演習

contact ページのテストを追加するだけなので割愛。

3.4.4演習

名前付きルート root_path を使うように変更します。

spec/requests/static_pages_spec.rb
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

spec/requests/static_pages_spec.rb
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の検証なので、システムスペックを使っていきます。

spec/system/static_pages_spec.rb
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で使えるようにします。

soec/rails_helper.rb
RSpec.configure do |config|
	# 省略...

  # 追記
  include ApplicationHelper
end

full_titleを導入します。

spec/helpers/application_helper_spec.rb
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

spec/requests/users_spec.rb
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 演習

spec/requests/users_spec.rb
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_all

Method: Capybara::Node::Finders#all — Documentation for jnicklas/capybara (master)

公式の記述によると、allfind_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さんの書いた記事です。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?