TECH EXPERT 29日目

TECH::EXPERT

テストコードの書き方が漠然としててよくわからん。
modelの評価はvalidかどうかを見るだけで、
controllerの評価はrender先とインスタンスが望んだものかを見るだけ。
なんだけど、新規データを保存する挙動の場合はインスタンスの中身の評価ではなくてデータテーブルに保存された行数を見ているのではなんでだろう?回りくどい上に正確なテストになってない気がするんだけど。

ruby

宇宙船演算子

A < Bなら-1を、A == Bなら0を、A > Bなら1を返す

sort

昇順で並び替え
array.sort
降順で並び替え
array.sort {|a, b| b<=>a }

compact, compact!(nilの要素を削除)

配列に対して使う。

select

条件に会う要素を取得する。

array.select { |a| 条件 }

delete_if

array = (1..5).to_a.delete_if { |el| el.even? }
array = (1..5).to_a.delete_if(&:even?)

テストコード

bundle exec rspec
bundle exec rspec spec/controllers/tweets_controller_spec.rb

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

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

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

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

gem rails-controller-testingのインストール

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

フォルダとファイル管理

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

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

getメソッド

httpメソッドをシンボル型にして渡す。(get, post, delete, patch)

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

render_templateマッチャ

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

matchマッチャ

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

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で比較している。

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の場合には表示されないので意味なし。

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

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

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

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

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

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

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...

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

FactoryBot

create_list

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

hoges = create_list(:hoge, 3)

 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}

テストのやり方

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

設定ファイルを作成

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

haml

inputタグ

submitボタンを設置する場合は{}内に続けてtypeを指定する。
ボタンの文字列はvalueに書く。

%input.form__input--btn{type: "submit", value: "Post"}

リセットCSS

使い方を。_reset.cssに書いてimportする。

コメント