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

# はじめに

Rails初学者の定番教材である[Railsチュートリアル](https://railstutorial.jp/)を完走しました。

完走後、[Railsチュートリアルの読み物ガイド](https://railstutorial.jp/reading_guide)に目を通してみて、
次のステップとして実践的なテストフレームワークであるRSpecについて学ぶことにしました。

そして、[Everyday Rails](https://leanpub.com/everydayrailsrspec-jp)を購入し、基本的なスペックが一通り書けるようになるまで進めました。

教材を通読・サンプルコードを書いた程度だとまだ理解が浅いので、
実践を通じてRSpecの使い方を理解するために**RailsチュートリアルのテストをMinitestからRSpecに置き換えてみる**ことにしました。

読み物ガイドで紹介されていた下の記事を答え合わせ的に使っていきます。

https://zenn.dev/fu_ga/books/ff025eaf9eb387

## 意識すること

- 過度にDRYであることよりも可読性を重視する。
    - ただ、ファクトリの扱いには慣れたいのでテストデータの用意はなるべくファクトリで。
- Railsチュートリアルに縛られすぎない。
    - 例えば「Railsチュートリアルがコントローラテストだったからコントローラスペックを書く」のような考え方は避ける。
- まず動かし、次に正しくし、それから速くする([Make it work, make it right, make it fast](http://wiki.c2.com/?MakeItWorkMakeItRightMakeItFast))」を意識する
    - 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](https://qiita.com/NasuPanda/items/f4b966eab333e4f3919e)
- [RailsチュートリアルのテストをRSpecに置き換えてみる【10章~12章】 - Qiita](https://qiita.com/NasuPanda/items/4c744c6a6ddbfa6d1393)
- [RailsチュートリアルのテストをRSpecに置き換えてみる【13章~14章+RuboCopの導入】 - Qiita](https://qiita.com/NasuPanda/items/2f66a18555132ab0766c)

https://github.com/NasuPanda/rails_practice/tree/5-rspec-layouts

# セットアップ

## 必要なgemのインストール

まずは`Gemfile` を編集して必要なgemを使えるようにします。

Railsのバージョンにもよりますが、
システムスペック用のcapybara、selenium-webdriver、webdribersはデフォルトでインストールされています。

```ruby
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` を実行します。

```bash
bundle install
```

## テストデータベースの設定

テスト実行時に接続するテスト用のデータベースを設定します。

```ruby
# spliteの場合
test:
<<: *default
database: db/test.sqlite3

# MySQLやPostgreSQLを使っている場合
test:
<<: *default
database: projects_test
```

## RSpecの設定

まず、次のコマンドを実行します。

```bash
$ 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` を追加して出力を読みやすくします。

```ruby:.rspec
--require spec_helper
--format documentation
```

### `binstub`  を使って短いコマンドで実行出来るようにする

RSpecを実行する際は `bundle exec rspec` のように毎回 `bundle exec` を付ける必要があります。

これは、実行ファイルがプロジェクトのコンテキストで実行されるようにするためです。

詳細 : [bundle execとbinstubを理解する - Qiita](https://qiita.com/NasuPanda/items/975328eddf4ea22465f8)

ただ、毎回 `bundle exec` を付けるのは面倒です。
`binstubs` コマンドを使うと少しだけコマンドを短くすることが出来ます。
次のコードを実行すると `bin/rspec` でテストを走らせることが出来るようになります。 

```bash
bundle binstubs rspec-core
```

また、この他にも独自のエイリアスを設定してコマンドを短くすることも出来ます。

### ジェネレータの設定

`rails genarate` を実行した時に、スペックファイルも一緒に生成されるようにします。
また、使用頻度の低いスペックファイルが生成されないようにします。

```ruby
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` コマンドを叩いてスペックファイルを生成します。

```bash
bin/rails g rspec:request static_pages
```

Railsチュートリアルではコントローラ用のテストを書いていました。

しかし、以下のような理由から**リクエストスペックとして実装していく**ことにしました。

- 現在コントローラスペックは公式に非推奨で、リクエストスペック/システムスペックへの移行が推奨されている。
    - 参考
    - http://rspec.info/blog/2016/07/rspec-3-5-has-been-released/#rails-support-for-rails-5
    - http://rspec.info/documentation/3.7/rspec-rails/#label-Controller+Specs
- テストしたいのは「適切なレスポンスが返ってくる」こと。

### 3.14

```ruby: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」は「〜を記述する」「〜を説明する」などの意味です。
    - ここでは「各アクションに対応するテストを書く」ということを説明/記述しています。
- 原則として、**example**（ `it` から始まる部分 ）**1つに付き1つの結果**を期待します。
    - このようにしておくと、テストが失敗した時に原因を特定しやすくなります。
- `expect` から始まる部分は**エクスペクテーション**と言い、ここで実際にテストを実行します。
    - テストする値を `expect` メソッドに渡し、それに続けてマッチャを呼び出します。
    - 「expect」は「〜を期待する」という意味です。
    - ここでは「ステータスコード200のレスポンスを期待する」という意味になります。
- `have_http_status` というマッチャを使ってレスポンスが正常に返ってきていることを確認しています。

テストを走らせてみます。

```bash
$ bin/rspec

# 省略...
StaticPages
  #home
    responds successfully
  #help
    responds successfully
  #about
    responds successfully
```

OKですね。

### 3.16

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

### 3.25

```ruby: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.body` と `include` マッチャを使いました。

### 3.4.3演習

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

### 3.4.4演習

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

```ruby: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で書き換える](https://zenn.dev/fu_ga/books/ff025eaf9eb387/viewer/372a93)

「リクエストスペックを使う」という点に関しては同じでした。
相違点について触れていきます。

### ステータスコードをシンボルに置き換え

`have_http_status "200"` と記述していましたが、シンボルに置き換え出来るようです。

> `have_http_status`というマッチャを使用してHTTPステータスコードが`:ok (200)`であるかを検証しています。
> 
> 
> ```ruby
> 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](https://qiita.com/terufumi1122/items/997e24dde87f807e3944#http%E3%82%B9%E3%83%86%E3%83%BC%E3%82%BF%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AE%E3%82%B7%E3%83%B3%E3%83%9C%E3%83%AB%E4%B8%80%E8%A6%A7)

シンボルの方がわかりやすいので今後はなるべくシンボルを使っていきます。

### `example` を表す文字列が日本語

`it` に渡す文字列が日本語です。

Everyday Railsでは全て英語だったので、そういうものかと思っていました。

[使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」 - Qiita](https://qiita.com/jnchito/items/42193d066bd61c740612#it-%E3%82%84-context-%E3%81%AB%E6%B8%A1%E3%81%99%E6%96%87%E5%AD%97%E5%88%97%E3%81%AF%E8%8B%B1%E8%AA%9E%E3%81%A7%E6%9B%B8%E3%81%8F%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%81%A7%E6%9B%B8%E3%81%8F)
[RSpec 導入時にチーム内で意識・決定しておきたいルール](https://zenn.dev/noraworld/articles/rspec-rules-for-team#%E8%87%AA%E7%84%B6%E8%A8%80%E8%AA%9E%E3%82%92%E7%B5%B1%E4%B8%80%E3%81%99%E3%82%8B)

↑の記事を見る限り、

- チームメンバー全員英語の読み書きに不自由しない(=頑張って英語を読み書きするためのコストが発生しない)なら英語が理想。
- そうでないなら日本語でもOK。
- チームで合わせることが大事。

という感じでしょうか。
どちらがスタンダードなのか知りたいな・・・

ひとまず英語で進めようと思います。

# 4章 Rails風味のRuby

## RSpecで書き換える

### 4.4

```ruby
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で書き換える](https://zenn.dev/fu_ga/books/ff025eaf9eb387/viewer/752cc2)

一緒でした。

# 5章 レイアウトを作成する

## RSpecで書き換える

### 5.21

3.4.3で実施したので割愛

### 5.28

```ruby: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の検証なので、システムスペックを使っていきます。

```ruby: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)](https://rubydoc.info/github/jnicklas/capybara/master/Capybara%2FNode%2FFinders:all)

### 5.37

`full_title` というヘルパーメソッドをRSpecで使えるようにします。

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

  # 追記
  include ApplicationHelper
end
```

`full_title`を導入します。

```ruby: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

```ruby: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 演習

```ruby: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で書き換える](https://zenn.dev/fu_ga/books/ff025eaf9eb387/viewer/b61000)

UIのテストにシステムスペックを使う点など、ほぼ一緒でした。

### 5.32

```ruby
# 自分が書いたテスト
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](https://rubydoc.info/github/jnicklas/capybara/master/Capybara/Result)
#all([kind = Capybara.default_selector], locator = nil, **options) {|element| ... } ⇒ [Capybara::Result](https://rubydoc.info/github/jnicklas/capybara/master/Capybara/Result)
**Also known as: find_all**
>
> [Method: Capybara::Node::Finders#all — Documentation for jnicklas/capybara (master)](https://rubydoc.info/github/jnicklas/capybara/master/Capybara%2FNode%2FFinders:all)

公式の記述によると、`all` と `find_all` は同じメソッドのようです。
返り値を引数に入れる、`find_all` を使用する方がわかりやすいので変更します。

```ruby
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のコード(コントローラの単体テスト)

```ruby
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のコード(リクエストスペック)

```ruby
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で書き換える](https://zenn.dev/fu_ga/books/ff025eaf9eb387)

RSpecのREADME.md
[https://github.com/rspec/rspec-rails](https://github.com/rspec/rspec-rails)

システムスペック/フィーチャスペック/リクエストスペックの違い
[System specs, feature specs, request specs–what’s the difference?](https://github.com/rspec/rspec-rails#system-specs-feature-specs-request-specswhats-the-difference)

CapybaraのREADME.md
[capybara/README.md at master · teamcapybara/capybara](https://github.com/teamcapybara/capybara/blob/master/README.md)

使えるRSpec入門
「Everyday Rails」の翻訳者である@jnchitoさんの書いた記事です。
- [使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」 - Qiita](https://qiita.com/jnchito/items/42193d066bd61c740612)
- [使えるRSpec入門・その2「使用頻度の高いマッチャを使いこなす」 - Qiita](https://qiita.com/jnchito/items/2e79a1abe7cd8214caa5)
    - 一通り目を通したい。
- [使えるRSpec入門・その3「ゼロからわかるモック（mock）を使ったテストの書き方」 - Qiita](https://qiita.com/jnchito/items/640f17e124ab263a54dd)
    - モックをあまり理解出来ていないのでそのうち読む。
- [使えるRSpec入門・その4「どんなブラウザ操作も自由自在！逆引きCapybara大辞典」 - Qiita](https://qiita.com/jnchito/items/607f956263c38a5fec24)
    - 逆引き的に使う。