【Rails/RSpec】Everyday Rails - RSpecによるRailsテスト入門の要約/感想

# はじめに
* 今回はRSpecの技術書である[Everyday Rails - RSpecによるRailsテスト入門](https://leanpub.com/everydayrailsrspec-jp)を読了したので、その要約と感想について書いていければと思います。

### RSpecとは？
* ruby用に開発された、テスト用の **フレームワーク** のことです。
* RSpecを用いることで、少ない記述で、可読性に優れたコードを簡単に記述することができます。


### 本記事の対象者
* railsのチュートリアルレベルを一通り学び終え、テストコードの学習を始めようと思っている方
* これから [Everyday Rails - RSpecによるRailsテスト入門](https://leanpub.com/everydayrailsrspec-jp) を読む前に要点を掴んでおきたい方 
* 実務でRSpecを使うことになり、キャッチアップの上で全体の概要を掴んでおきた方

を対象にこの記事を執筆しました。

ぜひ最後まで閲覧いただけると幸いです。


# よかったところ
### RSpecを深く、網羅的に学べる「唯一」の技術書
* 表題にあるように、本書はRSpecを **抜け漏れなく体系的** に学ぶことができます。
Ruby,Railsの技術書はたくさんありますが、私が確認できる中で **RSpec** 1つに絞り、200ページ以上に渡って解説されている技術書は本書以外にないと思われます。
ほとんどがrails,rubyの中の1要素として20pほど割いて、基礎的な部分を触れられる程度の形になっているのが現状です。
* なので、今後より安全で強固なシステムを作るためのテストについて、より深く、網羅的に学びたいという方は本書を手元において、参考にしながらコーディングしていくの求められると思われます。


### 著者の実務経験を基にしたRSpecのエッセンスを学ぶことができる
* 本書は著者が実務経験をする中で、 **現場で通用する実践的スキル** を中心に学ぶことができる技術書になっています。
以下引用文

> 本書の第二のゴールは日常的によく使う RSpec の機能と構文をあなたが使いこなせるように手助けすることです。
〜中略〜
Rails アプリケーションをテストする際に私が何年にもわたって使ってきたツールに焦点を絞って説明しています。

上記のあるように、実務に役立つノウハウに絞って学ぶことができ、その分難易度も高いですが、より実践的な知識を身につけることができる技術書になっています。



# 学んだこと

### 1.RailsにおけるRSpecテストとは
*  Railsにはデフォルトでテストコードの雛形が生成される。
*  しかし、railsのフレームワークを覚えることで手一杯になってしましい、テストまで手が回らない人が多い。
*  またテストの工数を割くことで、開発工数を減らすことに抵抗がある人が多く、ブラウザ上だけのテストで終わらせてしまう人も多くいる。
* どうしても開発に重きが置かれ、テストにおける学習は軽視されがち
* Railsにおいては、RSpecを使用すると読みやすいスペックが簡単に書くことができる。
* RSpecは複雑なフレームワークだが、著者が実務を通じて重要だと感じた単元に絞って本書では説明する。

#### 筆者が考えるテスト原則
- 絶対的に正しいテストの原則というものは存在しない
- その上で著者が掲げる原則が以下
    - テストは信頼できるものであること
    - テストは簡単に書けるもの
    - テストは簡単に理解できること
- 一番大切なのはテストが存在すること(テストコードがないパターンは多々ある)
- ブラウザ上の動きでテストするのではなく、テストコードを書き自動化することで、開発効率を高め、潜在的なバグや境界値に潜む問題を発見することができる

### 2.モデルスペック

- 最初はアプリケーションのコアとなるモデルからテストをはじめる。
    - railsで機能を構築するときもまずモデルから生成するので同様の手順
    - 既存のモデルの対して、モデルスペックを作る。

#### モデルスペックの基本構造

- モデルの状態が有効(valid)になっていること
- バリデーションを失敗させるテストであれば、モデルの状態が有効になっていること
- クラスメソッドとインスタンスメソッドが期待通り動作すること
- 例)
    - 期待する記述をまとめて記述している
    - example(``it``で始まる1行)一つにつき、結果を一つだけ期待している。
    - どのexampleも明示的である。
    ```ruby
    describe User do
      # 姓、名、メール、パスワードがあれば有効な状態であること
      it "is valid with a first name, last name, email, and password"
      # 名がなければ無効な状態であること
      it "is invalid without a first name"
      # 姓がなければ無効な状態であること
      it "is invalid without a last name"
      # メールアドレスがなければ無効な状態であること
      it "is invalid without an email address"
      # 重複したメールアドレスなら無効な状態であること
      it "is invalid with a duplicate email address"
      # ユーザーのフルネームを文字列として返すこと
      it "returns a user's full name as a string"
    end
    ```
#### should構文とexpect構文
* 過去にはshould構文を利用していたが、2012年以降はexpect構文に変わった。
* shouldはverによってはエラーになることがあるので、基本的にはexpect構文を用いる。

```ruby
# should構文
it "adds 2 and 1 to make 3" do
  (2 + 1).should eq 3
end

# expect構文
it "adds 2 and 1 to make 3" do
  expect(2 + 1).to eq 3
end
```
#### モデルスペックのサンプルコード
* `be_valid` というメソッドを用いてモデルが有効な状態かどうか検討
* testする際は実際にテストコードやソースコードを書き換えて、わざとtestを失敗させることも必要

```ruby
 require 'rails_helper'
  
RSpec.describe User, type: :model do
   # 姓、名、メール、パスワードがあれば有効な状態であること
   it "is valid with a first name, last name, email, and password" do
     user = User.new(
       first_name: "Aaron",
       last_name:  "Sumner",
       email:      "tester@example.com",
       password:   "dottle-nouveau-pavilion-tights-furze",
      )
      expect(user).to be_valid
   end
   
   # 　異常系テスト(first_nameが空欄のまま入力された場合)
   it "is invalid without a first name" do
     use = Use.new(first_name: nil)
     
     user.valid?
     expect(user.errors[:first_name]).to include("can't be blank")
   end
end

```

#### マッチャについて
* マッチャとは
  * 期待値と実際の値を比較して、一致した(もしくは一致しなかった)という結果を返すオブジェクト
    
<br>

* to/not_to/to_not
  * 「〜であること」を期待する場合は `to`
  ```ruby
    expect(1 + 2).to eq 3
  ```
  * 「〜でないこと」を期待する場合は `not_to` / `to_not` を使う。
  ```ruby
    expect(1 + 2).not_to eq 1
    # または
    expect(1 + 2).to_not eq 1
  ```
<br>

- eq

    - 「等しい」かどうかを検証する場合に使用する。
    
  ```ruby
    expect(1 + 2).to eq 3
  ```
<br>

- be_xxxx
    - be_xxxを使用することで、テストコードが自然な英文っぽく読めるようになる。
    - `empty?` のようにメソッドが「?」で終わり、戻り値が true/falseになるメソッドを `be_empty` のような形で検証することができる。
    
    ```ruby
    expect([]).to be_empty
    
    # 以下のコードと同様の意味になる
    expect([].empty?).to be true
    # または
    expect([].empty?).to eq true
   ```
    
    - `be_valid` を使用することで、Railsのmodelにバリデーションエラーが発生していないことをテストすることができる。
    
    ```ruby
    user = User.new(name: 'Tom', email: 'tom@example.com')
    expect(user).to be_valid # user.valid? が true になればパスする
    ```

<br>

- include
    - 「〜が含まれていること」を検証することができる。
    ```ruby
    expect([1, 2, 3]).to include 2
    ```
 
#### describe,context(テストのグループ化)

- describe
    - テストのグループ化を宣言するメソッド
    - グループ分けすることで、テストの可読性を高めることができる。
    
    ```ruby
    RSpec.describe '四則演算' do
      # itはテストをexampleという単位でまとめる役割をする。
      it '1 + 1 は 2 になること' do
        expect(1 + 1).to eq 2
      end
    end
    ```
    
    - ネストさせることも可能
    
    ```ruby
    RSpec.describe '四則演算' do
      describe '足し算' do
        it '1 + 1 は 2 になること' do
          expect(1 + 1).to eq 2
        end
      end
      describe '引き算' do
        it '10 - 1 は 9 になること' do
          expect(10 - 1).to eq 9
        end
      end
    end
    ```
<br>

- context
    - テストをグループ化するためのメソッド
    - `describe` と機能的には同じだが、 `context` は条件を分けたりするときに使うことが多い。
    
    ```ruby
    RSpec.describe User do
      describe '#greet' do
        context '12歳以下の場合' do
          it 'ひらがなで答えること' do
            user = User.new(name: 'たろう', age: 12)
            expect(user.greet).to eq 'ぼくはたろうだよ。'
          end
        end
        context '13歳以上の場合' do
          it '漢字で答えること' do
            user = User.new(name: 'たろう', age: 13)
            expect(user.greet).to eq '僕はたろうです。'
          end
        end
      end
    end
    ```

#### before
* 処理を共通化させたい時に使用する。
* `before do ... end` で囲まれた部分はexampleの実行前に毎回呼ばれる。
    
    ```ruby
    RSpec.describe User do
        describe '#greet' do
            before do
              @params = { name: 'たろう' }
            end
            context '12歳以下の場合' do
              it 'ひらがなで答えること' do
                user = User.new(**@params.merge(age: 12))
                expect(user.greet).to eq 'ぼくはたろうだよ。'
              end
            end
            context '13歳以上の場合' do
              it '漢字で答えること' do
                user = User.new(**@params.merge(age: 13))
                expect(user.greet).to eq '僕はたろうです。'
              end
            end
        end
    end
    ```
    



### 3.FactoryBot
#### Factory botとは

- Rspecでテストを行う際に「検証用のテストデータを生成する」Gemのこと
- 検証に必要なデータを生成し、使いまわすことが可能。
- テストが複雑になったときに、ファクトリを使えば、より可読性が高く、柔軟性の高いテストコードを記述することができる。

#### Factory botの基本的使い方

- `bin/rails g factory_bot:model user` コマンドでファイルの生成
- 生成されたファイルに下記のように記述することで、スペック全体でデフォルトデータのような形でファクトリを使用することができる。
- 共通部の切り出し、部品化のような形

  ```ruby
      1 FactoryBot.define do
      2   factory :user do
      3     first_name { "Aaron" }
      4     last_name  { "Sumner" }
      5     email { "tester@example.com" }
      6     password { "dottle-nouveau-pavilion-tights-furze" }
      7   end
      8 end
  ```

  ↓

* 切り出したFactoryを用いて、以下のような形でテストコードを簡潔な形で記述することができる。

    ```ruby
      1 require 'rails_helper'
      2 
      3 describe User do
      4   # 有効なファクトリを持つこと
      5   it "has a valid factory" do
      6     expect(FactoryBot.build(:user)).to be_valid
      7   end
      8 
      9   # 他のスペックが並ぶ ...
      10 end
    ```

#### ファクトリ内の重複をなくす（trait）

- traitとは
    - FactoryBotで複数のサンプルデータを作成する際に使われるテクニック
    - Factoryが複数ある場合、traitを使用することで、記述の重複を減らすことができ、可読性を高めたコードにすることできる。
    
    ```ruby
    FactoryBot.define do
      factory :モデル名 do
        trait :上記モデルを継承したインスタンスの属性値 do
      
        end
      end
    end
    ```
    

- traitを使うまでの過程
    - trait使用前の冗長なコード(重複箇所が多い)
    
    ```ruby
    FactoroyBot.define do
      factory :project do
        sequence(:name) { |n| "Test Project #{n}" }
        description "Sample project for testing purposes"
        due_on 1.week.from_now
        association :owner 
      end
    
      # 昨日が締め切りのプロジェクト
      factroy :project_due_yesterday, class: Project do
        sequence(:name) { |n| "Test Project #{n}" }
        description "Sample project for testing purposes"
        due_on 1.day.ago
        association :owner 
      end
    end
    ```
    
    - 継承を使うことで、「class:Project」と重複した記述が不要になり、以下のコードになる。
    
    ```ruby
    
    FactoroyBot.define do
      factory :project do
        sequence(:name) { |n| "Test Project #{n}" }
        description "Sample project for testing purposes"
        due_on 1.week.from_now
        association :owner
    
        # 昨日が締め切りのプロジェクト
        factroy :project_due_yesterday do
          due_on 1.day.ago
        end
      end
    end
    ```
    
    - 上記コードは親Factoryであるprojectを継承したproject_due_yesterdayが入れ子構造になっている。
    
    ```ruby
    factory :project
      factory :project_due_yestreday
    ```
    
    - traitを使用することで、継承するモデル名を省略し、以下のようなコードを記述することができる。
    
    ```ruby
    FactoroyBot.define do
      factory :project do
        sequence(:name) { |n| "Test Project #{n}" }
        description "Sample project for testing purposes"
        due_on 1.week.from_now
        association :owner
    
        # 昨日が締め切り
        trait :due_yesterday do
          due_on 1.day.ago
        end
      end
    end
    ```
    
    - specファイルでtraitを呼び出す場合は以下のような形で引数として渡す必要がある。
    ```ruby
    describe "latestatus" do
      # 締切日が過ぎていれば遅延していること
      it "is late when the due date is past today" do
        # 以下のように引数としてtraitを渡す必要がある
        project = FactoryBot.create(:project, :due_yesterday)
        expect(project).to be_late
      end
    end
    ```


  

### 4.コントローラスペック
- コントローラテストは簡単に追加することができるが、肥大化してしまうことがあるので注意
- 基本的にはアクセス制御が正しく動作しているかどうかを確認するテストに限定すること
- コントローラのテストは単体テストとして有効活用できるときに使うのにとどめて、使いすぎないようにすること

#### コントローラスペックの基本

- まず以下のコントローラを用意

```ruby
1 class HomeController < ApplicationController
2 
3   skip_before_action :authenticate_user!
4 
5   def index
6   end
7 end
```

- 上記コントローラのRSpecファイルを以下のコマンドで生成

```ruby
$ bin/rails g rspec:controller home --controller-specs --no-request-specs
```

↓(※rails7以降では自動的に生成される)

- `spec/controllers/home_controller_spec.rb`

```ruby
1 require 'rails_helper'
2 
3 RSpec.describe HomeController, type: :controller do
4 
5 end
```

- 基本テスト

```ruby
1 require 'rails_helper'
 2 
 3 RSpec.describe HomeController, type: :controller do
 4   describe "#index" do
 5     # 正常にレスポンスを返すこと
 6     it "responds successfully" do
 7       get :index
 8       expect(response).to be_successful
 9     end
10   end
11 end

```

#### 認証が必要なコントローラスペック

- ログインユーザー認証が必要な場合は `spac/rails_helper.rb` にDiviseヘルパーモジュールを組み込む

```ruby
1 RSpec.configure do |config|
2   # 設定ブロックの他の処理は省略 ...
3 
4   # コントローラスペックでDeviseのテストヘルパーを使用する
5   config.include Devise::Test::ControllerHelpers, type: :controller
6 end
```

- 上記を記述することで、ログイン状態をシュミレートすることができる。
- Diviseのヘルパーメソッドをテストコードの中で使用することも可能になる。

https://qiita.com/osugi_san/items/7faf51ea565cce10522a#request%E3%83%86%E3%82%B9%E3%83%88%E3%81%A7%E3%81%AE%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E5%87%A6%E7%90%86



### 5.システムスペック

- この章では、システム全体を一緒に使ってテストする
    - 単体のmodelやcontrollerが他のmodelやcotrollerとを結合させて、システム全体として、うまく動作することを確認する。
    - このようなテストを「システムスペック(受入テスト)」または「結合テスト」と呼ばれる。

#### システムスペックで使用するgem

- システムスペックでは、Copybaraを使用する。
    - webフォームの入力が可能
    - 画像の表示を検証することができる。
    - リンクのクリックをすることが可能。

```ruby
1 group :test do
2   gem 'capybara'
3 
4   # その他のgemは省略 ...
5 end
```

#### システムスペックの基本

- Capybaraでは `click_link` や `fill_in` 、`visit` といった理解しやすいメソッドが提供されていて、アプリケーションで必要な機能の シナリオ を書くことができる。
- 普段ブラウザで確認している動作をRSpecテスト用に言語化した感じ

```ruby
 1 require 'rails_helper'
 2 
 3 RSpec.describe "Projects", type: :system do
 4   before do
 5     driven_by(:rack_test)
 6   end
 7 
 8   # ユーザーは新しいプロジェクトを作成する
 9   scenario "user creates a new project" do
10     user = FactoryBot.create(:user)
11 
12     visit root_path
13     click_link "Sign in"
14     fill_in "Email", with: user.email
15     fill_in "Password", with: user.password
16     click_button "Log in"
17 
18     expect {
19       click_link "New Project"
20       fill_in "Name", with: "Test Project"
21       fill_in "Description", with: "Trying out Capybara"
22       click_button "Create Project"
23 
24       expect(page).to have_content "Project was successfully created"
25       expect(page).to have_content "Test Project"
26       expect(page).to have_content "Owner: #{user.name}"
27     }.to change(user.projects, :count).by(1)
28   end
29 end

```

#### CopybaraのDSL

- DSL
    - ある特定の目的を達成することだけに照準を絞った、独自の言語環境のこと
    - ここではCopybaraを用いてシステムスペックを行う際に使用するメソッドのこと
- Copybaraのメソッド一覧

https://qiita.com/jnchito/items/607f956263c38a5fec24


https://qiita.com/morrr/items/0e24251c049180218db4#dsl


https://github.com/teamcapybara/capybara#the-dsl


#### システムスペックをデバッグする

- テストが失敗する箇所に `save_and_open_page` を挟むことで、その直前で処理を止めて、ブラウザ上の動作を確認することができる。

```ruby
1 scenario "guest adds a project" do
2   visit projects_path
3   save_and_open_page
4   click_link "New Project"
5 end
```

- 上記の場合、毎回手作業でブラウザ開く必要がある
- *`Launchy* gem` をインストールすれば自動的にブラウザが開くようになる。

```ruby
1 group :test do
2   # Railsで元から追加されているgemは省略
3 
4   gem 'launchy'
5 end
```

- デバッグが終わったタイミングで`save_and_open_page` の記述は削除する。

#### JavaScriptを使った操作テスト

- CopyBaraはRack::Testというドライバで動作しているが、JavaScriptの動作をサポートしていない。
- `driven_by` メソッドの記述はなくし、加えて、ここでは `js: true` というオプション（タグ）を記述する。

```ruby
 1 require 'rails_helper'
 2 
 3 RSpec.describe "Tasks", type: :system do
 4   # ユーザーがタスクの状態を切り替える
 5   scenario "user toggles a task", js: true do
 6     user = FactoryBot.create(:user)
 7     project = FactoryBot.create(:project,
 8       name: "RSpec tutorial",
 9       owner: user)
10     task = project.tasks.create!(name: "Finish RSpec tutorial")

--略
```

- *`selenium-webdriver` のgemを使用する。*
- 使用するドライバは `driven_by` メソッドを使ってテストごとに変更することができ、これを共通設定(helperファイルに記述)とする。
- `spec/support/capybara.rb`

```ruby
RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by :rack_test
  end

　# js: trueと記述されている場合はselenium_chromeドライバを使用することにする。
  config.before(:each, type: :system, js: true) do
    driven_by :selenium_chrome
  end
end
```

- テスト実行中にブラウザを開かないようにするには `spec/support/capybara.rb` 以下のように記述する。(ヘッドレスドライバ)

```ruby
config.before(:each, type: :system, js: true) do
  driven_by :selenium_chrome_headless
end
```

#### システムスペックとフィーチャースペック

- システムスペックはRails 5.1から導入されたシステムテストを利用している。
- それ以前はフィーチャスペック（feature specs）と呼ばれるRSpec Rails独自の機能を使って統合テストを書いていた。
- フィーチャースペックとシステムスペックの相違点
    - *spec/system* ではなく *spec/features* にファイルを保存する
    - `describe` メソッドではなく `feature` メソッドを使う
    - `type:` オプションに `:system` ではなく `:feature` を指定する
- 原則フィーチャスペックではなく、システムスペックを使うようにする。
- フィーチャスペックはレガシーな機能になりつつあるため、早めにシステムスペックに移行する方が良い。








### テスト駆動開発において
- テスト駆動開発とは
    - 最初にテストを書き、そのテストが動作する必要最低限の実装を行った後で、コードを洗練させていくこと。
    - 別名テストファーストともいう
    - テストコードを書く→失敗→エラー文を元にコーディング→、、、、でシステムを作り上げていく感じ
    - テスト失敗のエラー文はどのようなコーディングをすればいいのかのヒントになる
    - テスト駆動開発の信条はは「テストを前進させるために必要最小限のコードを書く
- 異常系のテスト
    - 想定外の入力がなされた時に、それを考慮した出力結果がなされるかどうかのテスト
        - ex)ユーザー登録機能
            - 想定ではpasswordを4文字以上入力される形だったが、3文字しか入力されない場合にvalidationエラーが出力されるかどうか



# 難しかったこと

### RSpec独自の記法
* RSpecはrailsのライブラリの一つですが、記法に関してはRSPec独特の記法のようなものがあり、こちら実際にコードを書きながら慣れていく日宇町があるなと思いました。また、メソッド等もたくさんあるので、こちらも実践を通じて都度使いこなせるようになっていきたいと感じました。

### 実務でRSpecを使用したことないと理解が難しい
* 本書はRSpecの実務で役立つ知識に焦点を当てて記述されており、自分のような実務経験が少ないと理解するのが難しいと感じました。基礎構文を丁寧に解説というよりかは、実務でどのようにコーディングするのかをサンプルコードを用いて記述されている印象を受けました。
なので、RSpecを１回も触ったことがない、Railsでの実務経験があまりないという方は、まずは下記にて基礎部分をザッと確認してから読み進めるのがいいかもしれません。

https://qiita.com/jnchito/items/42193d066bd61c740612

* また、一度で全てを完全に理解するのは難しいので、6割理解でまずは１度読み通して、実際にコーディングしていく中で立ち返りながら、徐々に理解を深めていければいいと感じました。

# おわりに

ここまで[Everyday Rails - RSpecによるRailsテスト入門](https://leanpub.com/everydayrailsrspec-jp)の読了しての要約/感想記事を書かせていただきました。
Railsにおいてテストは開発と比べて、軽視されがちですが、質の高いシステムを作るのには欠かせないものとなります。
これを機にぜひとも、こちらの本書を通じて、RSpecの見識を深めるきっかかけになれば幸いです。
ここまで本記事を閲覧いただきありがとうございました！