hiyoko-programingの日記

プログラミングを勉強したてのひよっ子。   エンジニア目指して勉強中。

コントローラーのテスト

コントローラーのテストとは、コントローラー内のメソッドであるアクションが呼ばれた際の挙動をチェックするものになる。
1つのアクションにつき、以下の2点を確かめる。

1.アクション内で定義されているインスタンス変数の値が期待したものになるか
2.アクションの持つビューに正しく遷移するか

つまり、ひとつのアクションに対して2つ以上のexample(it '' do ~ end)が必要です。基本的には以下のような流れでコードを書く。

コントローラーのテストの流れ
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
describe ◯◯Controller do
  describe 'HTTPメソッド名 #アクション名' do
    it "インスタンス変数は期待した値になるか?" do
  "擬似的にリクエストを行ったことにするコードを書く"
      "エクスペクテーションを書く"
    end

    it "期待するビューに遷移するか?" do
      "擬似的にリクエストを行ったことにするコードを書く"
      "エクスペクテーションを書く"
    end
  end

各アクションはそれぞれリクエストされる際のhttpメソッドが違うが、それぞれ少しずつテストの書き方が異なる。ここでは、Railsに定められている7つのアクションのうちhttpメソッドがgetであるアクションに関してのテストコードの書き方を考える。

事前準備

まずは、Pictweetの元のコードを編集し、正常にテストコードが動作するようにする。

Pictweetの元のコードを編集

Deviseによって定義されるメソッドが利用されている箇所を無効にしておく。

 app/controllers/tweets_controller.rbを以下のように編集

各アクションが呼ばれる際にbefore_actionで発動する「ログインしていなかった場合にリダイレクトするメソッド」が呼ばれないよう、before_actionの行をコメントアウトしておく。

app/controllers/tweets_controller.rb
1
2
#3行目付近の以下のような行をコメントアウト
  # before_action :move_to_index, except: [:index]

 Gemfileに追記

Gemfile
1
2
3
4
5
group :development, :test do
   ~省略~
  gem 'rails-controller-testing'
   ~省略~
end

追記して保存したら、bundle installを実行。

続いて、コントローラーのテスト用Specファイルを作成し、基本となるコードを書き実行できるか確かめる。

Rspecのテストコードは、テストされるコードと同じ階層を作成して、そこに配置する。今回はapp/controllers以下のファイルに関するテストコードを作成するため、specディレクトリ以下にcontrollersディレクトリを作成する。

 Pictweetのディレクトリに、spec/controllers/を作成

上記で作成したspec/controllersの直下にtweets_controller.rbのテストコードを書いていくファイルtweets_controller_spec.rbを作成する。

 spec/controllers以下にtweets_controller_spec.rbを作成する

ここまでで、以下のようなディレクトリ構成になっていることを確認。

spec/controllersの作成

  •  pictweet
    •  app
    • 省略・・・
    •  spec
      •  models
        •  user_spec.rb
      •  controllers
        •  tweets_controller_spec.rb

 tweets_controller_spec.rbを以下のように編集

spec/controllers/tweets_controller_spec.rb
1
2
3
4
5
require 'rails_helper'

describe TweetsController do

end

モデルのテストコードと同様にrequire 'rails_helper'を記述した後、describeを利用し大枠としてクラス名でグループを作成する。

このコードが正常に動作するか確認。RSpecのテストコードを実行するにはbundle exec rspecコマンドを利用したが、specファイルが複数ある場合は特定のファイルを選択して実行することも可能。今回は、spec/controllers/tweets_controller_spec.rbを選択して実行してみる。

 ターミナルから以下のコマンドを利用し、spec/controllers/tweets_controller_spec.rbを実行

特定のSpecファイルの実行
1
$ bundle exec rspec spec/controllers/tweets_controller_spec.rb

その後、ターミナルに以下のように表示されればspecファイルは問題なく実行できている。

ターミナル
1
2
3
4
No examples found.

Finished in 0.06744 seconds (files took 3.46 seconds to load)
0 examples, 0 failures

これで準備は完了。

httpメソッドがgetで呼ばれるアクションのテストコードを書く

7つのアクションの中でも比較的簡単にテストコードを書くことができる「httpメソッドがget」であるアクションのテストコードを書いていく。
これは「index, new, edit, show」の4種類があるが、Pictweetにおいてはnewアクションが最も単純なアクション。
中身が何もないため、テストすべきことは「newアクションが動いたあとnew.html.erbに遷移するか」のみ。そこで、今回はまずnewアクションのテストコードから記述する。

newアクションのテストコード

newアクション用のテストコードのグループを作成する

spec/controllers/tweets_controller_spec.rb
1
2
3
4
5
6
7
8
9
require 'rails_helper'

describe TweetsController do
  describe 'GET #new' do
    it "renders the :new template" do
    end
  end

end

コントローラーのアクションもメソッドなので、名前の頭に#をつけるのは変わらない。違うのは、その前にhttpメソッド名を大文字で書き加えている点である。

また、it と do の間のメッセージは、今回テストしたい内容を書いている。

こちらのグループの中身に、以下のような流れでテストコードを書く。

 ・まず、擬似的にnewアクションを動かすリクエストを行うコードを書く
 ・次に、new.html.erbに遷移することを確かめるコードを書く

 

はじめに、「擬似的なリクエストを行うコード」を書き、擬似的なリクエストを実現するためにはメソッドを利用する。

 getメソッド

各httpメソッドには、それぞれ対応するメソッド(get, post, delete, patch)が存在する。引数として、利用したいコントローラーのアクションをシンボル型で渡す。必要なパラメーターが存在する場合は、各パラメーターをハッシュ形式で渡す。

【例】擬似的にshowアクションのリクエストをしたい場合

◯◯_controller_spec.rb
1
2
3
4
5
6
7
describe ◯◯Controller do
  describe 'GET #show' do
    it "renders the :show template" do
      get :show, params: {  id: 1 }
    end
  end
end

4行目でgetメソッドを利用している。擬似的にshowアクションを呼び出すリクエストなので、引数はアクション名と渡すパラメーターの2つ。
アクション名は:show、渡すパラメーターはidというキーに対して適切なオブジェクトである数字の1をバリューにしたハッシュ、id: 1

 

 tweets_controller_spec.rbを以下のように編集

spec/controllers/tweets_controller_spec.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
require 'rails_helper'

describe TweetsController, type: :controller do

  describe 'GET #new' do
    it "renders the :new template" do
      get :new

    end
  end

end

newアクションをリクエストするには引数は必要ないため、記述はget :newのみ。これで、「擬似的にnewアクションを動かすリクエストを行うコード」を書けた。これはこの後何度も出てくるので、是非書き方を覚えておく。

続いて、new.html.erbに遷移することを確かめるコードを書く。

spec/controllers/tweets_controller_spec.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
require 'rails_helper'

describe TweetsController, type: :controller do

  describe 'GET #new' do
    it "renders the :new template" do
      get :new
      expect(response).to render_template :new
    end
  end
end

8行目に記述を追加した。expectメソッドに対してresponseという引数を渡し、これとrender_template :newというメソッドの返り値を比較している。

 response

example内でリクエストが行われた後の遷移先のビューの情報を持つインスタンス

 render_templateマッチャ

引数にシンボル型でアクション名を取る。引数で指定したアクションがリクエストされた時に自動的に遷移するビューを返す。この部分は、他のアクションのテストに関してもrender_templateマッチャに対して適切なアクション名を渡すだけで大丈夫。

これで「newアクションが動いたあとnew.html.erbに遷移するか」のテストは完成。つまり、newアクションに関するテストコードは完成。試しに、一度テストを実行。

 spec/controllers/tweets_controller_spec.rbを実行

Specファイルの実行
1
$ bundle exec rspec spec/controllers/tweets_controller_spec.rb

以下のように表示されれば、テストは無事にパスしている。

ターミナル
1
2
3
4
5
6
TweetsController
  GET #new
    renders the :new template

Finished in 0.13692 seconds (files took 3.21 seconds to load)
1 example, 0 failures

editアクションのテストコード

続いて、editアクションのテストコードを書く。

先ほどのnewアクションでは、「アクションの持つビューに正しく遷移するか」のみをテストした。

しかし、tweets_controller.rbのeditアクションを確認すると、@tweetというインスタンス変数を定義していることがわかる。

tweets_controller.rb
1
2
3
  def edit
    @tweet = Tweet.find(params[:id])
  end

なので、今度はコントローラーのテストのもう一つの基本である「アクション内で定義されているインスタンス変数の値が期待したものになるか」についてのexampleも必要。
まずは、exampleを書く。

 

newアクションのテストコードに続ける形で、下記を追記。

spec/controllers/tweets_controller_spec.rb
1
2
3
4
5
6
7
8
9
#省略
  describe 'GET #edit' do
    it "assigns the requested tweet to @tweet" do
    end

    it "renders the :edit template" do
    end
  end
#省略

あらかじめ、2つのexampleを作成。まずは、上の方の「インスタンス変数の値を確かめる」テストコードを書く。

さて、editアクションが何をしているのかを考えると、まずはtweetsテーブルにレコードが入っていなければならない。そこで、このexampleの中でtweetsテーブルにレコードを保存する。そのためには、factory_botを利用する。

 spec/factories/tweets.rbを作成し、中身を以下のように編集

spec/factories/tweets.rb
1
2
3
4
5
6
7
FactoryBot.define do
  factory :tweet do
    text {"hello!"}
    image {"hoge.png"}
    user
  end
end

これで、factory_botを利用してtweetを作成できるようになった。tweetをデータベースに保存したいので、createメソッドを利用すれば良い。

spec/controllers/tweets_controller_spec.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#省略
  describe 'GET #edit' do
    it "assigns the requested tweet to @tweet" do
      tweet = create(:tweet)
    end

    it "renders the :edit template" do
    end
  end
#省略

4行目の記述を追加。
続いて、擬似的なリクエストする。今回もhttpメソッドはgetなので、getメソッドを利用する。

spec/controllers/tweets_controller_spec.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#省略
  describe 'GET #edit' do
    it "assigns the requested tweet to @tweet" do
      tweet = create(:tweet)
      get :edit, params: { id: tweet }
    end

    it "renders the :edit template" do
    end
  end
#省略

5行目の記述を追加した。params: { id: tweet }とすることで、idというキーのバリューに先ほど作成したインスタンスのidをセットすることができる。
取得するレコードの情報は、まさに一行上で作成しているTweetクラスのインスタンスtweet」なので、id: tweetとしている。

続いて、editアクションで取得しているインスタンス変数が、上記で作成した変数tweetと一致するかを確かめるためのエクスペクテーションを書く。そのために、expectメソッドの引数にassignsというメソッドを利用する。

 assignsメソッド

コントローラーのテスト時、アクションで定義しているインスタンス変数をテストするためのメソッド。
引数に、直前でリクエストしたアクション内で定義されているインスタンス変数をシンボル型で取る。
通常はexpectメソッドの引数としてよく利用する。

今回editアクションで定義しているインスタンス変数の名前は@tweetなので、assignsメソッドの引数は:tweetとなる。

spec/controllers/tweets_controller_spec.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#省略
  describe 'GET #edit' do
    it "assigns the requested tweet to @tweet" do
      tweet = create(:tweet)
      get :edit, params: { id: tweet }
      expect(assigns(:tweet)).to eq tweet
    end

    it "renders the :edit template" do
    end
  end
#省略

6行目を追記した。こうすることで、リクエストされたeditアクションの中で定義されている@tweetが、間違いなくこちらで作成しているtweetとなっているかが確かめられる。

これで、「アクション内で定義されているインスタンス変数の値が期待したものになるか」のテストコードを書くことができた。

もうひとつのexampleである「期待したビューに遷移するか」のテストコードを書いてみる。

 

「editアクションをリクエストした後、edit.html.erbに遷移するか」を確かめるテストコードを書く。以下を追記できればeditアクションのテストは完了。

spec/controllers/tweets_controller_spec.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#省略
  describe 'GET #edit' do
    it "assigns the requested tweet to @tweet" do
      tweet = create(:tweet)
      get :edit, params: { id: tweet }
      expect(assigns(:tweet)).to eq tweet
    end

    it "renders the :edit template" do
      tweet = create(:tweet)
      get :edit, params: { id: tweet }
      expect(response).to render_template :edit
    end
  end
#省略

indexアクションのテストコード

続いて、indexアクションのテストコードを書く。
indexアクションに関してのポイントは、indexアクションで定義している@tweetsは配列の形で取得されてくるということ。

まずは空のexampleを作成する。

editアクションのテストコードに続ける形で、下記を追記。

spec/controllers/tweets_controller_spec.rb
1
2
3
4
5
6
7
8
9
#省略
  describe 'GET #index' do
    it "populates an array of tweets ordered by created_at DESC" do
    end

    it "renders the :index template" do
    end
  end
#省略

上の方のexampleが、インスタンス変数の値を確かめるためのもの。tweetsコントローラーのindexアクションでは、まずテーブルから全てのレコードを取得してきているため、テストの際も複数のレコードが存在しなければならない。

tweets_controller.rbのindexアクション
1
2
3
4
5
class TweetsController < ApplicationController

  def index
    @tweets = Tweet.includes(:user).page(params[:page]).per(5).order("created_at DESC")
  end

そこで、indexアクションのexampleではまずはじめに、tweetsテーブルに複数のレコードを作成することからはじめる。そのためには、factory_botcreate_listメソッドを利用する。

 create_list

factory_botの設定ファイルに存在しているリソースを複数作成したい場合に以下のように利用できる。

create_listの例
1
hoges = create_list(:hoge, 3)

第一引数に作成したいリソースをシンボル型で、第二引数に作成したい個数を数字で渡す。
上記の式では、hogeというリソースを3つ作成しレコードに保存している。

これを利用して、tweetのリソースを3つ、作成する。コントローラーのspecファイルにてcreate_listメソッドを利用する。

spec/controllers/tweets_controller_spec.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#省略
  describe 'GET #index' do
    it "populates an array of tweets ordered by created_at DESC" do
      tweets = create_list(:tweet, 3) 
      get :index
    end

    it "renders the :index template" do
    end
  end
#省略

4, 5行目を書き加えた。tweetのレコードを3つ保存した上で、そのインスタンスtweetsという変数に代入している。
5行目では、indexアクションへの擬似的なリクエストを行っている。

続いてuserのfactoryの内容を変更する。

spec/factories/users.rb
1
2
3
4
5
6
7
8
FactoryBot.define do
  factory :user do
    nickname              {"abe"}
    password              {"00000000"}
    password_confirmation {"00000000"}
    sequence(:email) {Faker::Internet.email}
  end
end

tweetを3つ作ると、それに伴ってuserも3名分作成される。その時に、同じemailを使用するとバリデーションに引っかかってエラーになる。そのためFakerを使って異なるものになるように実装する。

 Faker

emailや電話番号、名前などのダミーデータを作成するためのGem。インストール後、factory_botの設定ファイルの中でFakerのメソッドを利用し、ダミーデータを生成する。

 Fakerをインストール

Gemfileの下部に、以下の記述を追加。これは、test環境でのみ読み込まれるという記述方法。

Gemfile
1
2
3
group :test do
  gem 'faker', "~> 2.8"
end

その後、忘れずにbundle installコマンドを実行。

【例】Fakerダミーデータの生成方法

1
2
{ Faker::Internet.email }
=> "rodrick.wyman@rosenbaum.org"

続いてindexアクションで定義されているインスタンス変数@tweetsの値を確かめるエクスペクテーションを記述する。期待される値が配列の場合は、matchというマッチャを利用。

 matchマッチャ

引数に配列クラスのインスタンスをとり、expectの引数と比較するマッチャ。配列の中身の順番までチェックする。

 

matchマッチャを利用してエクスペクテーションを書く。

spec/controllers/tweets_controller_spec.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  describe 'GET #index' do
    it "populates an array of tweets ordered by created_at DESC" do
      tweets = create_list(:tweet, 3) 
      get :index
      expect(assigns(:tweets)).to match(tweets)
    end

    it "renders the :index template" do
    end
  end
#省略

この状態でテストを実行すると、テストはパスする。しかし、このままでは厳密にテストを作成することはできていない。tweets_controller.rbを見ると、indexアクションで定義されているインスタンス変数@tweetsは、created_atで降順にソートしているからである。この状態を再現しなければならない。

現状、create_listメソッドで生成しているtweetのレコードのcreated_atの値は全て同じになってしまっている。これをランダムな値にするために先ほどのFakerを利用する。

tweets.rb
1
2
3
4
5
6
7
8
FactoryBot.define do
  factory :tweet do
    text {"hello!"}
    image {"hoge.png"}
    created_at { Faker::Time.between(from: DateTime.now - 2, to: DateTime.now) }
    user
  end
end

これで、tweetのリソースひとつひとつのcreated_atはランダムに生成される。続いて、example内で定義しているtweetsの中身の順番を、created_atを基準に降順で並び替える。そのためには、sortメソッドを利用する。

spec/controllers/tweets_controller_spec.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  describe 'GET #index' do
    it "populates an array of tweets ordered by created_at DESC" do
      tweets = create_list(:tweet, 3) 
      get :index
      expect(assigns(:tweets)).to match(tweets.sort{ |a, b| b.created_at <=> a.created_at } )
    end

    it "renders the :index template" do
    end
  end
#省略

5行目で、sortメソッドを利用している。

これで、indexアクションに関するテストコードは半分完成。

あとは、ページ遷移のexampleである。

spec/controllers/tweets_controller_spec.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
  describe 'GET #index' do
    it "populates an array of tweets ordered by created_at DESC" do
      tweets = create_list(:tweet, 3)
      get :index
      expect(assigns(:tweets)).to match(tweets.sort{|a, b| b.created_at <=> a.created_at })
    end

    it "renders the :index template" do
      get :index
      expect(response).to render_template :index
    end
  end

参考書籍

Everyday Rails - RSpecによるRailsテスト入門

https://leanpub.com/everydayrailsrspec-jp/read