単体テスト(rails)

rspecを用いて行う。

rspec

テストコードを書く際の原則

  • 各exampleで期待する値は1つ
  • 期待する結果をはっきりわかりやすく記述
  • 起きて欲しいことと起きてほしくないこと両方をテストする
  • 境界値をテストする
  • 可読性を考えつつ、適度にDRYにする

rspecの導入

gemfileに以下を追記し、bundle install

group :development, :test do
  gem 'rspec-rails'
end

group :development do
  gem 'web-console'
end

rspec設定ファイルを作成

rails g rspec:install

specフォルダに以下が作成される。

create  .rspec
create  spec
create  spec/spec_helper.rb
create  spec/rails_helper.rb

rails_helper.rb

共通の設定を書いておくファイル。
各テスト用ファイルに読み込ませ、共通の設定やメソッドを適用する。

spec_helper.rb

rails_helper.rbと同じくRSpec用の共通の設定を書いておくファイル。
こちらはRSpecをRails無しで利用する際に利用する。

フォーマット指定

.specファイルにフォーマットを指定できる。
デフォルトではProgressになっている。

  • progress (略:p)
  • documentation (d)
  • html (h)
  • json (j)
--format documentation

progressの場合、以下のような意味。

  • . 成功
  • F 失敗
  • * 未実装
User
  #create
    is invalid without a nickname
  #create
    is invalid without a email (FAILED - 1)

↑documentation
↓progress

.F

ファイル管理

project/spec/models
project/spec/controllers
のようにそれぞれモデル・コントローラーで対応するフォルダを分ける。
specファイルは対応するクラス名_spec.rbという名前にする。

テストコード

describe "hogehoge" do
  it "1 + 2は3になること" do
    expect(1 + 2).to eq 3
  end
end

describeでテストとしての塊を作る。””内にはテストの説明を書く。
クラスみたいな感じ。
以下のdo~endまでがテスト内容。

it “”で実際に動作させるコードの説明を書く。
メソッドみたいな感じ。エクスペクテーションと呼ぶ。

実行内容はexpect()内に書き、equalで期待する結果を書く。
ここはメソッド内の実行式みたいな感じ。
equalのほか、incledeやvalidなどがある。マッチャと呼ぶ。

テストの実行

テストを実行するときは以下のコマンドを使う

全てのテストファイルを動かす場合
bundle exec rspec

ファイルを指定する場合
bundle exec rspec spec/controllers/tweets_controller_spec.rb

モデルに対するテスト

CarrierWaveを使っているModelをRSpecでテスト

rspecでテストする際に画像を渡す場合は単に文字列を渡してはダメ。
任意の場所にファイルを置いて、file.openで画像を渡す。

image {File.open("#{Rails.root}/public/images/test_image.jpg")}

パスを連結する方法はjoinでも良い

Rails.root.join('spec/support/**/*.rb')

コントローラーに対するテスト

1つのアクションにつき、以下の2点を確かめる。

  • インスタンス変数の値が期待したものになるか
  • ビューに正しく遷移するか
describe Hogehoge_Controller do
  describe 'HTTPメソッド名 #アクション名' do
    it "インスタンス変数は期待した値になるか?" do
      "擬似的にリクエストを行ったことにするコードを書く"
      "エクスペクテーションを書く"
    end

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

コントローラーテスト用のgemをインストール

group :development, :test do
  gem 'rails-controller-testing'
end

フォルダとファイル管理

spec/controllersフォルダを作成し、以下にcontrollerに対応したspecファイルを作成する。

例
hogehoge_controller.rb に対して
hogehoge_controller_spec.rbを作成。

疑似的なHTTPリクエスト

テストしたいコントローラのHTTPリクエストとアクションを指定する。
httpメソッドをシンボル型にして渡す。(get, post, delete, patch)
必要に応じてparamsの情報も付与する。

describe 'GET #show' do
  it "renders the :show template" do
    get :show, params: {  id: 1 }
  end
end

response

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

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

assignsメソッド

アクションで定義しているインスタンス変数をテストするためのメソッド。
引数に、直前でリクエストしたアクション内で定義されているインスタンス変数をシンボル型でとる。

例
it "assigns the requested tweet to @tweet" do
  tweet = create(:tweet)
  get :edit, params: { id: tweet }
  expect(assigns(:tweet)).to eq tweet
end

同名のワードが何度も出てくるのに、それぞれ意味が違うので注意。

tweet = create(:tweet)
get :edit, params: { id: tweet }
expect(assigns(:tweet)).to eq tweet

青字のtweetは、FactoryBotへ渡す引数。factories/tweets.rbを示す。
赤字のtweetは、example内で生成したインスタンス。
緑字のtweetは、赤字のtweetを元にget #editを動かした結果、生成されたインスタンス。
この赤と緑のtweetをassignsで比較している。

letメソッド

複数のexampleで同一のインスタンスを使いたい場合、letメソッドを使う。
遅延評価(lazy evaluation)のため、呼び出し時に一度評価された後は値がキャッシュされ、以降の呼び出し時に同じ値を吐く。かつキャッシュされているため処理が高速化する。

以下の例の場合、groupとuserは各expectationで使い回してOKなのでletでインスタンスを生成する。

describe MessagesController do
  let(:group) { create(:group) }
  let(:user) { create(:user) }

  describe '#index' do

    context 'log in' do
      before do
        login user
        get :index, params: { group_id: group.id }
      end

      it 'assigns @message' do
        expect(assigns(:message)).to be_a_new(Message)
      end

      it 'assigns @group' do
        expect(assigns(:group)).to eq group
      end

      it 'redners index' do
        expect(response).to render_template :index
      end
    end
  end
end

before

beforeブロックの内部に記述された処理は、各exampleが実行される直前に、毎回実行される。

before do
  login user
  get :index, params: { group_id: group.id }
end

it do
end...

context

context do ~ endで囲むと結果がブロックとして表示される。

context 'can save' do
  it "is valid without a image" do
  end
  it "is valid without a body" do
  end
  it "is valid with body & image" do
  end
end
Message
  #create
    is invalid without image&body
    is invalid without a group_id
    is invalid without a user_id
    can save
      is valid without a image
      is valid without a body
      is valid with body & image

contextで囲まれていないものが自動的に先に表示される。
contextが複数ある場合は、上から順に並ぶ。

なおformat progressの場合には表示されないので意味なし。

matchers(マッチャ)

主にModelテスト用

eqマッチャ

同じかどうか。

includeマッチャ

含まれるかどうか。
モデルのテストで使う場合にはエラーメッセージを入れて使ったりする。

be_valid

validationがtrueかどうか。

it "is valid with nickname & email & password" do
  user = build(:user)
  user.valid?
  expect(user).to be_valid
end

be_validマッチャ

expectの引数にしたインスタンスが全てのバリデーションをクリアする場合にパスする。

expect(user).to be_valid

エラーメッセージの種類

  • is too short
  • is too long
  • can’t be blank
  • doesn’t match Password
  • has already been taken

shortやlongはエラーメッセージの配列の1つ目を取ってくる必要あり。
user.errors[:name][0]とせず、user.errors[:name]とした場合、
“is too short (minimum is 6 characters)” といった文字で返ってくる。

password_confirmationのエラーを誘導する場合、Pが大文字なのに注意。

主にControllerテスト用

render_templateマッチャ

引数にシンボル型でアクション名を取る。
引数で指定したアクションがリクエストされた時に自動的に遷移するビューを返す。

matchマッチャ

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

be_a_newマッチャ

対象が引数で指定したクラスのインスタンスかつ未保存のレコードであるかどうか確かめる。

it 'assigns @message' do
  expect(assigns(:message)).to be_a_new(Message)
end

redirect_toマッチャ

引数にとったプレフィックスにリダイレクトした際の情報を返す。

context 'not log in' do
  before do
    get :index, params: { group_id: group.id }
  end

  it 'redirects to new_user_session_path' do
    expect(response).to redirect_to(new_user_session_path)
  end
end

changeマッチャ

引数が変化したかどうかを確かめる。
change(Message, :count).by(1)と記述し、Messageモデルのレコードの総数が1個増えたかどうかを確かめる

.to .not_to

以下と同じことを示す場合は.toを。
違うことを示す場合には.not_toを。

it 'does not count up' do
  expect{ subject }.not_to change(Message, :count)
end

テスト例 一覧まとめ

基本フォーマット

下の例は継ぎはぎしているのでそのままでは意味が通りません。
requireが必要だったり、letやcontextなどはこの位置で使うんだなぁ程度で捉えてください。

require 'rails_helper'

describe MessagesController do
  let(:group) { create(:group) }
  describe '#index' do
    context 'log in' do
      it 'redirects to new_user_session_path' do
        post :create, params: params
        expect(response).to redirect_to(new_user_session_path)
      end
    end
  end
end

派生フォーマット

HTTPリクエストをbefore doでやる場合

describe '#index' do
  before do
    get :index, params: { group_id: group.id }
  end
  it 'redirects to new_user_session_path' do
    expect(response).to redirect_to(new_user_session_path)
  end
end

HTTPリクエストをsubjectでまとめる場合

describe '#index' do
  subject {
    post :create,
    params: invalid_params
  }
  it 'renders index' do
    subject
    expect(response).to render_template :index
  end
  it 'does not count up' do
    expect{ subject }.not_to change(Message, :count)
  end
end

expect例

インスタンスを評価する場合

expect(assigns(:group)).to eq group
expect(assigns(:message)).to be_a_new(Message)

viewを評価する場合

expect(response).to redirect_to(new_user_session_path)
expect(response).to render_template :index

データベースを評価する場合

expect{ subject }.to change(Message, :count).by(1)
expect{ subject }.not_to change(Message, :count)

factory_bot

インスタンスの生成を楽にしてくれる。
rspecと併用するので以下のようにgemファイルへ記入してbundle install

group :development, :test do
  gem 'rspec-rails'
  gem 'factory_bot_rails'
end

specディレクトリ直下にfactoriesディレクトリを作成。
テストしたいインスタンスの複数形のファイル名でRubyのファイルを作成。

例
spec/factories/user.rb

FactoryBot.define do

  factory :user do
    nickname              {"hogehoge"}
    email                 {"aaa@aaa.com"}
    password              {"12345678"}
    password_confirmation {"12345678"}
  end

end

こうすることで以下のように書ける。

describe User do
  describe '#create' do
    it "is invalid without a nickname" do
      user = build(:user, nickname: nil)
      user.valid?
      expect(user.errors[:nickname]).to include("can't be blank")
    end
  end
end

buildメソッド

引数にクラス名をシンボルで指定するとfactoriesファイルを参照し、自動的にインスタンスが生成される。インスタンス以外を続けて指定すると上書きできる。

user = FactoryBot.build(:user)
user = build(:user, nickname: nil)

buildなのは昔の名残???newと同義と思って良さそう。

createメソッド

createの場合はテスト用のDBに値が保存する。
ただし、テスト実行ごとにDB内容はロールバックされるため、保存した値は消えるので確認できない。DBの中身を確認したい場合はbinding.pry等で処理を止めること。

user = FactoryBot.create(:user)

create_list

リソースを複数作成したい場合に利用する。
第1引数にリソースを、第2引数に作成したい数を指定する。

hoges = create_list(:hoge, 3)

attributes_for

FactoryBotによって定義されるメソッドで、オブジェクトを生成せずにハッシュを生成する。
paramsの中身を作る際に使う。

let(:invalid_params) { { group_id: group.id, user_id: user.id, message: attributes_for(:message, body: nil, image: nil) } }

FactoryBotの省略記法

helperのconfigureに下記を記述することで冒頭のようにbuild(:user)でインスタンスを作成できるようになる。

spec/rails_helper.rb

RSpec.configure do |config|
  #下記の記述を追加
  config.include FactoryBot::Syntax::Methods
end

 Faker

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

FactoryBot.define do
  factory :user do
    nickname              {"abe"}
    password              {"00000000"}
    password_confirmation {"00000000"}
    sequence(:email) {Faker::Internet.email}
  end
end

fakerのインストール

テスト環境にのみ必要なのでgemfileの以下に記述しbundle install

group :test do
  gem 'faker'
end

fakerパターン例

email 重複回避
sequence(:email) {Faker::Internet.email}
email {Faker::Internet.free_email}

deviseをテストで利用する

ユーザーのログイン状態で検証項目が変わるため、準備が必要。
rspec下にsupportフォルダを作成し、module用にファイルを作成。

/spec/support/controller_macros.rb

module ControllerMacros
  def login(user)
    @request.env["devise.mapping"] = Devise.mappings[:user]
    sign_in user
  end
end

helperに、deviseのコントローラのテスト用のモジュールと先ほど定義したControllerMacrosを読み込む記述をする。

/spec/rails_helper.rb

RSpec.configure do |config|
  Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
  config.include Devise::Test::ControllerHelpers, type: :controller
  config.include ControllerMacros, type: :controller
  #〜省略〜
end

テストのやり方 まとめ

4つのgemをインストール

web-consoleを入れていなければweb-consleも。

group :development, :test do
  gem 'rspec-rails'
  gem 'factory_bot_rails'
  gem 'rails-controller-testing'
  gem 'faker'
end

group :development do
  gem 'web-console'
end

記入したらbundle install。
厳密にはfakerはtestのみで十分です。

設定ファイルを作成

rails g rspec:install

specフォルダに以下が作成される。

create  .rspec
create  spec
create  spec/spec_helper.rb
create  spec/rails_helper.rb

.rspecファイルに必要に応じて以下を追記

--format documentation

フォルダ作成

project/spec/models
project/spec/controllers

疑問

テストの疑問

前提としてfactorybotに以下の記述があるとして、

factories/message.rb

FactoryBot.define do
  factory :message do
    body   {Faker::Lorem.sentence}
    image  {File.open("#{Rails.root}/public/images/cat1.png")}
    user
    group
  end
end
it "is valid without a body" do
  message = build(:message, body: nil)
  binding.pry
  message.valid?
  expect(message).to be_valid
end

build直後で止めてmessageの中身を確認すると、すべてnil。
にも関わらずmessage.valid?はtrue。

pry> message
=> #<Message:0x00007f89391d5d48 id: nil, body: nil, image: nil, created_at: nil, updated_at: nil, user_id: nil, group_id: nil>

pry> message.valid?
=> true

bodyとimageはnull許可だけど、
validates :body, presence: true, unless: :image?
でモデルに片方は必要だとvalidationを掛けている。
どっちもnilでvalid?がtrueなのは何故だろう?

ではbuildをcreateにすると、どうなるか?

it "is valid without a body" do
  message = create(:message, body: nil)
  binding.pry
  message.valid?
  expect(message).to be_valid
end

こちらは値が入ってvalid?もOK.

pry> message
=> #<Message:0x00007feb4da4e198
 id: 3,
 body: nil,
 image: "cat1.png",
 created_at: Sat, 10 Aug 2019 10:37:08 UTC +00:00,
 updated_at: Sat, 10 Aug 2019 10:37:08 UTC +00:00,
 user_id: 3,
 group_id: 3>

pry> message.valid?
=> true

不思議なのはbuildした段階ではimageに値が入っていないけど、saveすると値が入ってくること。しかもvalidationのチェックは値が入ったものとして行われている。何故だろう?
ちなみにbodyはbuildした段階で既に値が入っている。これが普通の挙動だと思うけどなぁ。

外部キーのエラーメッセージの格納場所はどこ?

user_idを外部キーに持つmessageテーブルを保存した時、エラーメッセージの格納場所がmessage.errors[:user_id]になるのかと思いきやmessage.errors[:user]に格納される。
has_many through で持っている影響だと思うけど、わかりにくい。

it "is invalid without a user_id" do
  message = build(:message, user_id: nil)
  message.valid?
  expect(message.errors[:user]).to include("を入力してください")
end

コメント