【Rails】テストファイルで使用するコマンドとメソッド

# 記事概要
Ruby on Railsのテストについて、まとめる

# RSpec
https://qiita.com/likapantrip/items/5bff444bf5edfe4f1436

# Capybara
結合テストコードを記述するための仕組みであり、デフォルトでRailsに搭載されている

# まとめ(コマンド)
## rspecコマンド
specディレクトリ以下に書かれたRSpecのテストコードを実行するコマンド

# まとめ(テスト整理のメソッド)
## describeメソッド
テストコードのグループ分けを行うメソッド
「どの機能に対してのテストを行うか」を`describe`でグループ分けし、その中に各テストコードを記述する

## contextメソッド
テストコードのグループ分けを行うメソッド
使用方法は`describe`と同じだが、`context`には特定の条件を指定する

## itメソッド
テストコードのグループ分けを行うメソッド
「`describe`メソッドに記述した機能において、どのような状況のテストを行うか」を明記する

### example
`it`で分けたグループのこと

# まとめ(メソッド)
## expectation
検証で得られた挙動が想定通りなのかを確認する構文のこと
雛形は、`expect().to matcher()`

## matcher
「expectの引数」と「想定した挙動」が一致しているかどうかを判断する
どのような挙動を想定しているかを記述する

### include
「expectの引数」に「includeの引数」が含まれていることを確認するマッチャ

```ruby
# 配列の中に'メロン'が含まれていることを想定
expect(['りんご', 'バナナ', 'ぶどう', 'メロン']).to include('メロン')
```

### eq
「expectの引数」と「eqの引数」が等しいことを確認するマッチャ

```ruby
# 1 + 1という計算の結果が、2と等しいことを想定
expect(1 + 1).to eq(2)
```

### be_valid
`valid?`メソッドの返り値が、`true`であることを期待するマッチャ

expectの引数に指定されたインスタンスがバリデーションでエラーにならない場合、`valid?`の返り値は`true`となる

```ruby
@user = FactoryBot.build(:user)
expect(@user).to be_valid
#=> 「@user.valid?」の結果が"true"の場合、正常完了
#=> 「@user.valid?」の結果が"false"の場合、エラー発生
```

### have_content
`expect(page).to have_content('X')`と記述することで、pageの中に、Xという文字列があるかどうかを判断するマッチャ

```ruby
# 新規登録ボタンがあることを確認する
expect(page).to have_content('新規登録')
```

### have_no_content
`have_content`の逆で、文字列が存在しないことを確かめるマッチャ

### change
モデルのレコードの数がいくつ変動するのかを確認できるマッチャ

```ruby
# サインアップボタンを押すとユーザーモデルのカウントが1上がることを確認する
expect{
  find('input[name="commit"]').click
  sleep 1
}.to change { User.count }.by(1)
```

### have_current_path
pageのURLを確認するマッチャ

```ruby
# トップページへ遷移することを確認する
expect(page).to have_current_path(root_path)
```

### have_selector
指定したセレクタが存在するかどうかを判断するマッチャ

```ruby
# 要素「<div class="content_post" style="background-image: url(画像のURL);">」を取得
have_selector ".content_post[style='background-image: url(#{@tweet_image});']"

# トップページには先ほど投稿した内容のツイートが存在することを確認する（画像）
expect(page).to have_selector ".content_post[style='background-image: url(#{@tweet_image});']"
```

### have_link
要素の中に当てはまるリンクがあることを確認するマッチャ
`a要素`に対して使用する

`expect('要素').to have_link 'ボタンの文字列', href: 'リンク先のパス'`と記述することで、要素の中に当てはまるリンクがあることを確認できる

### have_no_link
have_linkの逆で、要素の中に当てはまるリンクがないことを確認するマッチャ

`expect('要素').to have_no_link 'ボタンの文字列', href: 'リンク先のパス'`と記述することで、要素の中に当てはまるリンクがないことを確認できる

### have_field
inputやtextareaなどのフォーム要素が存在するかを確認するマッチャ
引数には、CSSで使用している`#`などは不要

```ruby
# 「tweet_image」というidを持ったフォームが存在するかを確認
have_field('tweet_image')

# 「tweet_image」というidを持ったフォームが存在し、そのフォームにHelloと入力されていることを確認
have_field('tweet_image', with: "Hello")
```

## build
ActiveRecordの`new`メソッドと同様の意味を持つ
テスト用DBへアクセスしないため、データが保存されない

```ruby
# FactoryBotを利用しない場合
user = User.new(nickname: 'test', email: 'test@example', password: '000000', password_confirmation: '00000000')

# FactoryBotを利用する場合
user = FactoryBot.build(:user)
```

## create
ActiveRecordの`create`メソッドと同様の意味を持つ

`build`とほぼ同じ働きをするが、`create`の場合はテスト用DBに値が保存される
注意すべき点は、1回のテストが実行され、終了する毎にテスト用DBの内容がロールバックされる(テスト実行時に保存された値がすべて消去されてしまう)

```ruby
# FactoryBotを利用しない場合
user = User.create(nickname: 'test', email: 'test@example', password: '000000', password_confirmation: '00000000')

# FactoryBotを利用する場合
user = FactoryBot.create(:user)
```

## before
それぞれのテストコードを実行する前に、セットアップを行うこと

```モデル名_spec.rb
require 'rails_helper'
RSpec.describe モデル名, type: :model do
  before do
    # 処理(変数を受け渡す場合、インスタンス変数にする必要がある)
  end

  describe 'X' do
    it 'Y' do
      # before内の処理が完了してから実行される
    end
    it 'Z' do
      # before内の処理が完了してから実行される
    end
  end
end
```

## after
任意の処理後に指定の処理を実行する

## Rails.root
Railsアプリケーションのトップ階層のディレクトリまでの絶対パスを取得できる

```ruby
# /Users/ユーザー名/projects/に「sample-app」というRailsアプリがある場合
[1] pry(main)> Rails.root
=> #<Pathname:/Users/ユーザー名/projects/sample-app>
```

## Rails.root.join
引数として渡した文字列でのパス情報を、Rails.rootのパスの情報につけることができる

```ruby
# /Users/ユーザー名/projects/に「sample-app」というRailsアプリがある場合
[1] pry(main)> Rails.root.join('public/images/test_image.png')
=> #<Pathname:/Users/ユーザー名/projects/sample-app/public/images/test_image.png>
```

## create_list
FactoryBotの設定ファイルに存在しているレコードを、複数作成したい場合に使用するメソッド
`create_list`を用いることで、一度に複数のテストデータを生成可能

```ruby
# hogesテーブルのレコードを3つ作成
FactoryBot.create_list(:hoge, 3)

# hogesテーブルのレコードを3つ作成し、titleカラムの情報を「Hello world」とする
FactoryBot.create_list(:hoge, 3, title: 'Hello world')

# hogesテーブルのレコードを3つ作成し、外部キー(user_id)の情報を@user.idとする
FactoryBot.create_list(:hoge, 3, user_id: @user.id)
```

# まとめ(単体テストのメソッド)
## valid?メソッド
バリデーションを実行させて、エラーがあるかどうかを判断するメソッド

エラーがない場合は`true`を返す
エラーがある場合は`false`を返し、エラーの内容を示すエラーメッセージを生成する

```ruby
user.valid?
```

## errors
インスタンスにエラーを示す情報がある場合、その内容を返すメソッド

```ruby
# 変数userに新規データを格納
[1] pry(main)> user = User.new(nickname: '', email: 'test@example', password: '000000', password_confirmation: '000000')
=> #<User id: nil, email: "test@example", created_at: nil, updated_at: nil, nickname: "">

# 変数userにバリデーションを実行し、エラーがあるかを判断
[2] pry(main)> user.valid?
  User Exists? (0.4ms)  SELECT 1 AS one FROM `users` WHERE `users`.`email` = BINARY 'test@example' LIMIT 1
=> false

# エラー情報を表示
[3] pry(main)> user.errors
=> #<ActiveModel::Errors [#<ActiveModel::Error attribute=nickname, type=blank, options={}>]>
```

## full_messages
エラーの内容から、エラーメッセージを配列として取り出すメソッド

```ruby
# 変数userに新規データを格納
[1] pry(main)> user = User.new(nickname: '', email: 'test@example', password: '000000', password_confirmation: '000000')
=> #<User id: nil, email: "test@example", created_at: nil, updated_at: nil, nickname: "">

# 変数userにバリデーションを実行し、エラーがあるかを判断
[2] pry(main)> user.valid?
  User Exists? (0.5ms)  SELECT 1 AS one FROM `users` WHERE `users`.`email` = BINARY 'test@example' LIMIT 1
=> false

# エラーメッセージを表示
[3] pry(main)> user.errors.full_messages
=> ["Nickname can't be blank"]
```

```user_spec.rb
user.valid?
expect(user.errors.full_messages).to include("Nickname can't be blank")
```

## get
「パスにリクエストすると」を行う

```ruby
get Prefix名_path

# ルートパスにリクエストする
get root_path

# showアクションにリクエストする
get tweet_path(@tweet)
```

## response
リクエストに対するレスポンスそのもの

レスポンスで取得できる情報に、想定する内容が含まれているかを確認することで、テストコードを書ける

```ruby
[1] pry(#<RSpec::ExampleGroups::TweetsController::GETIndex>)> response
=> #<ActionDispatch::TestResponse:0x0000000117117278
 @cache_control={:max_age=>"0", :private=>true, :must_revalidate=>true},
 @committed=false,
 @cv=
  #<MonitorMixin::ConditionVariable:0x00000001171902b8
   @cond=#<Thread::ConditionVariable:0x0000000117190290>,
   @monitor=#<Monitor:0x0000000117190470>>,
 @header=
  {"X-Frame-Options"=>"SAMEORIGIN",
   "X-XSS-Protection"=>"0",
   "X-Content-Type-Options"=>"nosniff",
   "X-Download-Options"=>"noopen",
   "X-Permitted-Cross-Domain-Policies"=>"none",
   "Referrer-Policy"=>"strict-origin-when-cross-origin",
   "Link"=>
    "</assets/application-9b8e2e0d675fa110d5daab5bb63deb13a118ca78232c286410d1b5291170c08e.css>; rel=preload; as=style; nopush",
   "Content-Type"=>"text/html; charset=utf-8",
   "ETag"=>"W/\"77f2ac4a81f863be39016e9731728738\"",
   "Cache-Control"=>"max-age=0, private, must-revalidate",
   "X-Request-Id"=>"f1f6e0b0-b6fc-4f8e-81e1-127901377b68",
   "X-Runtime"=>"0.327373",
   "Content-Length"=>"3460"},
# 十字キーの↓を入力すると続きを確認でき、qを押下すると終了して次の入力画面に移行できる
```

### HTTPステータスコード
HTTP通信において、どのような処理の結果となったのかを示すもの

|ステータスコード|内容|
|-|-|
|100~|処理の継続中|
|200~|処理の成功|
|300~|リダイレクト|
|400~|クライアントのエラー|
|500~|サーバーのエラー|

### status
`response.status`を実行することで、HTTPステータスコードを確認できる

```ruby
# レスポンスのステータスコードが出力される
[1] pry(#<RSpec::ExampleGroups::TweetsController::GETIndex>)> response.status
=> 200
```

```tweets_spec.rb
expect(response.status).to eq 200
```

### body
`response.body`を実行することで、ブラウザに表示されるHTMLの情報を抜き出せる

```ruby
# ブラウザに表示されるHTMLの情報が出力される
[1] pry(#<RSpec::ExampleGroups::TweetsController::GETIndex>)> response.body
=> "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Pictweet</title>\n    <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n    \n    \n\n    <link rel=\"stylesheet\" href=\"/assets/application-9b8e2e0d675fa110d5daab5bb63deb13a118ca78232c286410d1b5291170c08e.css\" data-turbo-track=\"reload\" />\n    <script type=\"importmap\" data-turbo-track=\"reload\">{\n  \"imports\": {\n    \"application\": \"/assets/application-37f365cbecf1fa2810a8303f4b6571676fa1f9c56c248528bc14ddb857531b95.js\",\n    \"@hotwired/turbo-rails\": \"/assets/turbo.min-f3765a09513ca1417099ce92257ef54b9d4cf3a7addc7dcbb1d6d848d307ee8a.js\",\n    \"@hotwired/stimulus\": \"/assets/stimulus.min-dd364f16ec9504dfb72672295637a1c8838773b01c0b441bd41008124c407894.js\",\n    \"@hotwired/stimulus-loading\": \"/assets/stimulus-loading-3576ce92b149ad5d6959438c6f291e2426c86df3b874c525b30faad51b0d96b3.js\",\n    \"controllers/application\": \"/assets/controllers/application-368d98631bccbf2349e0d4f8269afb3fe9625118341966de054759d96ea86c7e.js\",\n    \"controllers/hello_controller\": \"/assets/controllers/hello_controller-549135e8e7c683a538c3d6d517339ba470fcfb79d62f738a0a089ba41851a554.js\",\n    \"controllers\": \"/assets/controllers/index-31a9bee606cbc5cdb1593881f388bbf4c345bf693ea24e124f84b6d5c98ab648.js\"\n  }\n}</script>\n<link rel=\"modulepreload\" href=\"/assets/application-37f365cbecf1fa2810a8303f4b6571676fa1f9c56c248528bc14ddb857531b95.js\">\n<link rel=\"modulepreload\" href=\"/assets/turbo.min-f3765a09513ca1417099ce92257ef54b9d4cf3a7addc7dcbb1d6d848d307ee8a.js\">\n<link rel=\"modulepreload\" href=\"/assets/stimulus.min-dd364f16ec9504dfb72672295637a1c8838773b01c0b441bd41008124c407894.js\">\n<link rel=\"modulepreload\" href=\"/assets/stimulus-loading-3576ce92b149ad5d6959438c6f291e2426c86df3b874c525b30faad51b0d96b3.js\">\n<link rel
```

```tweets_spec.rb
expect(response.body).to include(@tweet.text)
```

# まとめ(結合テストのメソッド)
## visit
指定したページへ遷移できる

```ruby
visit Prefix名_path

# トップページに移動する
visit root_path
```

## page
`visit`で訪れた先のページの見える分だけの情報が格納されている
カーソルを合わせてはじめて見ることができる文字列は含まれない

## fill_in
`fill_in 'フォームの名前', with: '入力する文字列'`と記述することで、フォームへ入力できる

[![Image from Gyazo](https://i.gyazo.com/0360afbd32d0f91767b29d56ed798a71.png)](https://gyazo.com/0360afbd32d0f91767b29d56ed798a71)

```ruby
# ユーザー情報を入力する
fill_in 'Nickname', with: @user.nickname
fill_in 'Email', with: @user.email
fill_in 'Password', with: @user.password
fill_in 'Password confirmation', with: @user.password_confirmation
```

## all
指定した要素を全て取得する

```ruby
# pageに存在する同名のクラスを持つ要素をまとめて取得
all('クラス名')

# 「◯番目のmoreクラス」を取得
all('クラス名')[0]
```

## find
指定した要素を取得する

```ruby
find('要素')
```

## find().click
`find('クリックしたい要素').click`と記述することで、実際にクリックできる

```ruby
# <input type="submit" name="commit" value="Sign up" data-disable-with="Sign up">をクリックする
find('input[name="commit"]').click
```

## find_link().click
`find_link('リンクの文字列', href: 'URL').click`と記述することで、a要素で表示されているリンクをクリックする

## hover
特定の要素にカーソルをあわせたときの動作を再現する

```ruby
# user_navクラスの中にあるspan要素がログアウトボタン
# カーソルを合わせるとログアウトボタンが表示されることを確認する
expect(
  find('.user_nav').find('span').hover
).to have_content('ログアウト')
```

## click_onメソッド
引数に文字列を取り、一致するテキストなどを持った要素をクリックできるメソッド

※()をつけるかは自由

```ruby
# 「<a href="/contents/1">詳しくはこちら</a>」の要素をクリックしたことにできる
click_on ('詳しくはこちら')

# 「<input type="submit" name="commit" value="ログイン">」の要素をクリックしたことにできる
click_on ('ログイン')
```

## attach_fileメソッド
画像などのアップロード用のinput要素(タイプがfileのinput要素)に、テスト用画像を添付(アタッチ)できるメソッド

- 第一引数
    - 画像をアップロードするinput要素のname属性の値
- 第二引数
    - アップロードする画像のパス
- 第三引数以降
    - オプション

```ruby
attach_file('input要素のname属性の値', 画像のパス)
```

```ruby
# 添付する画像を定義
image_path = Rails.root.join('public/images/test_image.png')

# 画像選択フォームに画像を添付する
attach_file('message[image]', image_path)
```

### make_visible
CSSの`display: none;`を設定していることで、非表示になっている画像選択フォームの場合に付与するオプション

```ruby
# 非表示の画像選択フォームに画像を添付する
attach_file('message[image]', image_path, make_visible: true)
```

## fixture_file_uploadメソッド
インスタンスに画像ファイルをセットするメソッド

# Ruby on Railsまとめ
https://qiita.com/likapantrip/items/46b43c9d1309b6633f0a