RSpec 基本まとめ01(Model, FactoryBot, Controller)

[Everyday Rails - RSpecによるRailsテスト入門](https://leanpub.com/everydayrailsrspec-jp)でRSpecの勉強を行っているので、今後見返すことができるようまとめてみました。

### 関連記事
- [RSpec導入手順](https://qiita.com/mitsuooo/items/59886ccf5a1cbaad5537)

# describe, context, it の使い分け

- describe : テストの対象を記述
- context : 特定の条件を記述

# モデルスペック

## 特徴
- モデルをテストすればアプリケーションのコアとなる部分をテストできる ⇨ 土台が堅牢になり、信頼性の高いコードベースの構築につながる

## RSpecファイル作成

```
# models/user.rbの場合

bin/rails g rspec:model user
```

```spec/models/user_spec.rb
# RSpecに対し、ファイル内のテストを実行するためにRailsアプリケーションの読み込みが必要
require 'rails_helper'

RSpec.describe User, type: :model do
  pending "add some examples to (or delete) #{__FILE__}"
end
```

## マッチャ

### be_valid
モデルが有効な状態か検証

```spec/models/user_spec.rb
# 例
.
.
it "is valid user" do
  user = User.new(
    name: 'test',
    email: 'tester@example.com',
  )
  expect(user).to be_valid
end
```

### include
バリデーションのテスト時等に使用

```spec/models/user_spec.rb
# 例
.
.
it "is invalid without an email" do
  user = User.new(
    name: 'test',
    email: nil,
  )
  # 有効でないカラムに、error内容が格納される
  user.valid?
  expect(user.errors[:email]).to include("can't be blank")
end
```

＊必要な値が空：**"can't be blank"**
＊値が重複している：**"has already been taken"**

バリデーションのテストを追加することで、テストを書いている最中に、モデルが持つべきバリデーションについて考える機会を設けることができる。

### be_empty

空の値であるか検証

### eq
期待する値と一致しているか検証

例（性と名を連結して返すインスタンスメソッドをテスト）

```app/models/users.rb
def name
  [firs_tname, last_name].join(' ')
end
```

```spec/models/user_spec.rb
it "returns a user's full name as a string" do
  user = User.new(
    first_name: 'test',
    last_name: 'user',
    email: 'tester@example.com'
  )
  expect(user.name).to eq 'test user'
end
```

同様に**クラスメソッド**や**スコープ**もテスト可能

### to ⇨ to_not / not_to

期待する値を反転させる
to_not / not_to に違いはない。どちらの方が文章として適切かで使い分けてOK

# FactoryBot

## インストール

```:Gemfile
group :development, :test do
  gem "rspec-rails"
  gem "factory_bot_rails"
  .
end
```

```
bundle install
```

## 新規ファクトリファイルの生成

```
# 例　spec/factories/users.rb

bin/rails g factory_bot:model user
```

```spec/factories/users.rb
FactoryBot.define do
  factory :user do
    first_name "test"
    last_name "user"
    email "tester@example.com"
    .
    .
  end
end
```

## テスト

### 有効なファクトリか検証

```spec/models/user_spec.rb
require 'rails_helper'

describe User do
  it "has a valid factory" do
    # インスタンス化されるだけで、保存はしていない
    expect(FactoryBot.build(:user)).to be_valid
  end
end
```

### ファクトリを使用したバリデーションの検証

```spec/models/user_spec.rb
# 名がなければ無効な状態であること
it "is invalid without a first name" do
  # 検証対象をnilに置き換え
  user = FactoryBot.build(:user, first_name: nil)
  user.valid?
  expect(user.errors[:first_name]).to include("can't be blank")
end
```

### シーケンスを使ったユニークなデータ生成

```spec/factories/users.rb
FactoryBot.define do
  factory :user do
    first_name "test"
    last_name "user"
    sequence(:email) { |n| "tester#{n}@example.com" } 
  end 
end
```

複数userが同じ値を持たないよう設定したい場合は、上記のコードのようにカウンタの値を1つずつ増やすことで、ユニークな値を生成することができる

### ファクトリで関連づけ

(例) `user` `post` `comment` の3つのモデル間の関連づけ

```spec/factories/comments.rb
FactoryBot.define do
  factory :comment do
    comment "test comment"
    # ファクトリでの関連づけ
    association :post
    association :user
  end
end
```

```spec/factories/posts.rb
FactoryBot.define do
  factory :post do
    sequence(:name) { |n| "Post #{n}" }
    # aliasを設定
    association :owner
  end
end
```

```spec/factories/users.rb
FactoryBot.define do
  # aliasを設定
  factory :user, aliases: [:owner] do
    first_name "test"
    last_name "user"
    sequence(:email) { |n| "tester#{n}@example.com" }
  end
end
```

関連づけを行うと、下記のように子モデルのインスタンスを生成すると、その子に関連した`post`と`user`が用意される

```spec/models/comment_spec.rb
.
.
# ファクトリで関連データを生成
it "generates associated data from a factory" do
  comment = FactoryBot.create(:comment)
end
```

しかし、このままでは、userが2人生成されている。
なぜなら、`comment`生成時にその親である`post`が生成、次にそのpostの親である`user1=owner`が生成される。
その後に、`spec/factories/comments.rb`で設定した、`association :user`によって、`user2`が生成される

この問題の対処法は、下記のコードのように変更を加え、userを関連づけで生成された`post.owner`に設定する

```spec/factories/comments.rb
.
.
  association: posts
  user { project.owner }
end
```

ファクトリはまれに、想定していない不必要なデータが生成されていることがあるので注意が必要である。

また、`alias`を設定すると、`app/models/post.rb`が以下のように変更されている。

```app/models/post.rb
.
.
  belongs_to :owner, class_name: User, foreign_key: :user_id
  has_many :comments
.
end
```

### be_late どう載せるか

## trait
ファクトリの重複を減らす方法として、変更したい属性値だけをまとめて設定する`trait`が存在する。

（例）締め切りが存在する仕事(task)をファクトリで生成

```spec/factories/tasks.rb
.
factory :task do
  sequence(:name) { |n| "Task #{n}" }
  due_on 1.week.from_now
  association :owner

  # 昨日が締め切りのプロジェクト
  trait :due_yesterday do
    due_on 1.day.ago
  end

  # 今日が締め切りのプロジェクト
  trait :due_today do
    due_on Date.current.in_time_zone
  end

  # 明日が締め切りのプロジェクト
  trait :due_tomorrow do
    due_on 1.day.from_now
  end
end
.
```

```spec/models/task_spec.rb
.
# 締め切り日が過ぎている
it " is late when the due date is past today" do
  task = FactoryBot.create(:task, :due_yesterday)
  expect(task).to be_late
end

# 締め切り日が今日でスケジュール通り
it "is on time when the due date is today" do
  task = FactoryBot.create(:task, :due_today)
  expect(task).to_not be_late
end

# 締め切り日がまだ先ならスケジュール通り
it "is on time when the due date is in the future" do
  task = FactoryBot.create(:task, :due_tomorrow)
  expect(task).to_not be_late
end
.
```

## コールバック

ファクトリでもcallbackを使用することができる

（例）task生成時に、memoを3つ生成

```spec/factories/tasks.rb
.
factory :task do
  sequence(:name) { |n| "Task #{n}" }
  due_on 1.week.from_now
  association :owner

  # memo付きのtask
  trait :with_notes do
    after(:create) { |task| create_list(:note, 3, task: task) }
  end
  .
end
.
```

create_listメソッドでは、モデルを作成するための関連モデルが必要

#### テスト

```spec/models/task_spec.rb
it "can have many notes" do
  task = FactoryBot.create(:task, :with_notes)
  expect(task.notes.length).to eq 3
end
```

詳しい使い方は、
- [FactoryBotの公式ドキュメント](https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md)
- [FactoryBotの公式ドキュメントにあるコールバック](https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md#callbacks)

# コントローラスペック

コントローラのテストは公式に格下げされた。統合テストに置き換えることが推奨されるが、基本を勉強する上では、学びがあるためまとめてみます。

## ファイル生成

```
# 例
bin/rails g rspec:controller post
```

## テスト例

### 正常にレスポンスを返すことを検証 & 200レスポンスを返すことを検証

- `response` : ブラウザに返すべきアプリケーションの全データを保持しているオブジェクト
- `be_success` : レスポンスステータスが成功(200)か、それ以外であるかをチェック

```ruby
describe "#index" do
  it "responds successfully" do
    get :index
    expect(response).to be_success
  end

  it "returns a 200 response" do
    get :index
    expect(response).to have_http_status "200"
  end
end
```

### 認証（Devise）が必要なテスト

Deviseは認証が必要なコントローラのアクションに対して、ユーザーのログイン状態をシミュレートするヘルパーを提供しているため、設定を追加することで使用可能。

```spec/ralis_helper.rb
RSpec.configure do |config|
.
.
  # コントローラスペックで、Deviseのテストヘルパーを使用できるよう設定
  config.include Devise::Test::ControllerHelpers, type: :controller
end
```

上記のように設定することで、`sign_in`ヘルパーを使用することができるようになる。

```ruby
.
before do
  @user = FactoryBot.create(:user)
end

# 認証済みのユーザー
it "responds successfully" do
  sign_in @user
  get :index
  expect(response).to be_success
end

# ゲストユーザー
context "as a guest" do
  # 302レスポンスを返すことを検証
  it "returns a 302 response" do
    get :index
    expect(response).to have_http_status "302"
  end
 
  # サインイン画面にリダイレクトすること
  it "redirects to the sign-in page" do
    get :index
    expect(response).to redirect_to "./users/sign_in"
  end
end
```

- `redirect_to` : コントローラが認証されていないリクエストの処理を中断し、指定した画面に移動することを検証

**＊has_secure_passwordメソッドを使用している場合の例**

```ruby
def sign_in(user)
  cookies[:auth_token] = user.auth_token
end
```

### showアクションのテスト

`#index`との違いは、GETリクエストの際にparamsを設定する点

```ruby
describe "#show" do
.
  before do 
    @user = FactoryBot.create(:user)
    @post = FactoryBot.create(:post, owner: @user)
  end
.
  it "responds successfully" do
    sign_in @user
    get :show, params: { id: @post.id }
    expect(response).to be_success
  end
.
end
```

### POST

- POSTの際は、paramsを渡す
- `attributes_for(:post)`は、プロジェクトファクトリからテスト用の属性値をハッシュとして作成する

```ruby
describe "#create" do
  before do
    @user = FactoryBot.create(:user)
  end 

  it "adds a post" do
    post_params = FactoryBot.attributes_for(:post)
    sign_in @user
    expect {
      post :create, params: { post: post_params }
    }.to change(@user.posts, :count).by(1)
  end 
end
```


### PATCH

- PATCHの時も、更新するオブジェクトのidと、変更する値を渡す
- `reload`メソッドを使ってDB上の値を更新しないと、メモリに保存された値が再利用されて李舞、値の変更が反映されないので注意

```ruby
describe "#update" do
  before do
    @user = FactoryBot.create(:user)
    @post = FactoryBot.create(:post, owner: @user)
  end 

  it "updates a post" do
    post_params = FactoryBot.attributes_for(:post, name: "New Post Name")
    sign_in @user
    patch :update, params: { id: @post.id, post: post_params }
    expect(@post.reload.name).to eq "New Post Name"
  end 
end
```

### DESTROY

- destroyメソッドには、DELETEリクエストでアクセス
- DESTROYの際も、削除するオブジェクトのidをparamsで渡す

```ruby
describe "#destroy" do
  before do
    @user = FactoryBot.create(:user)
    @post = FactoryBot.create(:post, owner: @user)
  end 

  it "deletes a post" do
    sign_in @user
    expect {
      delete :destroy, params: { id: @posts.id }
    }.to change(@user.posts, :count).by(-1)
  end 
end
```

### ユーザー入力のエラーをテスト

- テストでは、何か正しくないことが怒った時も意図した通りの動きになるかを検証するべきである。
- 今回は無効の値を設定する`trait`を用意して記述

```spec/factories/posts.rb
FactoryBot.define do
  factory :post do
    sequence(:name) { |n| "Test Project #{n}" }
    description "Sample project for testing purposes"
    due_on 1.week.from_now
    association :owner
    
    .
    .
  
    trait :invalid do
      name nil
    end
  end
end
```

```ruby
before do 
  @user = FactoryBot.create(:user)
end

it "does not add a post" do
  post_params = FactoryBot.attributes_for(:post, :invalid)
  sign_in @user
  expect {
    post :create, params: { post: post_params }
  }.to_not change(@user.posts, :count)
end
```

### HTML以外の出力

- ここまでのテストしたコントローラのアクションは全て`text/html`フォーマット
- 明示的に記述することで、JSON形式でレスポンスを返すことをテストすることができる
- `response.content_type`でフォーマットの形式を取得することができる

```spec/controllers/comments_controller_spec.rb
before do
  @user = FactoryBot.create(:user)
  @post = FactoryBot.create(:post, owner: @user)
  @comment = @post.comments.create!(name: "Test Comment")
end

describe "#show" do
  # JSON形式でレスポンスを返すことを検証
  it "responds with JSON formatted output" do
    sign_in @user
    # フォーマットを指定
    get :show, format: :json, 
      params: { post_id: @post.id, id: @comment.id }
    expect(response.content_type).to eq "application/json"
  end
end
```




## 参考
- [Everyday Rails - RSpecによるRailsテスト入門](https://leanpub.com/everydayrailsrspec-jp)