naichi's lab

3日後の自分(他人)への書き置き

Ruby on Rails、RSpecを使ってコントローラのテストを書いてみる

f:id:naichilab:20160116161543p:plain

Ruby on Railsを使って以前作ったサイトの再構築をしてます。

色々調べながらRSpecでコントローラのテストを書いてみたので手順をメモ。

Rails、RSpecともに初心者なので間違ってたらどんどん指摘ください。

環境

  • ruby 2.2.2p95
  • Rails 4.2.2
  • RSpec 3.1.7

テスト対象

Ruby on Rails チュートリアル:実例を使って Rails を学ぼう

よくありがちなUserControllerを対象にしてみます。(ほぼRailsチュートリアルで作ったもの)

Railsで、URLにIDでなく名前を入力して、アクセスする方法 - Qiita

作ってるサイトでは上記記事を参考に、/users/naichilabみたいなURLでアクセスできるようにしています。

UserControllerに関係するrake routesはこんな感じ

                  users GET    /users(.:format)                           users#index
                        POST   /users(.:format)                           users#create
               new_user GET    /users/new(.:format)                       users#new
              edit_user GET    /users/:permalink/edit(.:format)           users#edit
                   user GET    /users/:permalink(.:format)                users#show
                        PATCH  /users/:permalink(.:format)                users#update
                        PUT    /users/:permalink(.:format)                users#update
                        DELETE /users/:permalink(.:format)                users#destroy

RSpecのインストール

参考書籍:Read Everyday Rails - RSpecによるRailsテスト入門 | Leanpub

自分はこの本を参考にインストールした。とてもわかりやすい。

FactoryGirlの設定方法とかも全部この本の通りにやった。

コントローラのテストって何するの?

参考サイト:Rubyist Magazine - スはスペックのス 【第 2 回】 RSpec on Rails (コントローラとビュー編)

古い記事だけど上記の記事を読んでかなり理解が進みました。

  • RSpecで行えるのは単体テスト
  • 単体テストでは責務をテストする
  • コントローラの責務とは??

コントローラの責務

  1. 受信したリクエストに対して適切なレスポンスを返す
    • リクエストに対してHTTPレスポンスがステータスコード200を返す。とか。
  2. ビューで使用するのに必要なモデルオブジェクトをロードする
    • リクエストされたURLから必要なモデルインスタンスをロードしておく。とか。
  3. レスポンスを表示するのに適切なビューを選択する
    • 適切なテンプレートを表示している。とか。

ふむふむ。それをかけばいいのね。

テスト書いてみる

どこに書くの?

/spec/controllers/users_controller_spec.rbに書けばいいみたい。

アウトラインを書く

参考書籍にある通り、最初はUsersControllerbefore_actionを無効化して書き始めることにする。

class UsersController < ApplicationController
  # before_action :logged_in_user, only: [:edit, :update, :destroy]
  # before_action :correct_user,   only: [:edit, :update]
  # before_action :admin_user,     only: :destroy

ややこしくなりそうだから一歩ずつね。

rake routesで出てくる7つのルーティングに対して先ほどの3つの責務は何か?を考えながら書いてみた。

require 'rails_helper'

describe UsersController do

  describe 'Get #new' do
    it 'リクエストは200 OKとなること'
    it '@userに新しいユーザーを割り当てること'
    it ':newテンプレートを表示すること'
  end

  describe 'Get #index' do
    it 'リクエストは200 OKとなること'
    it '@usersに全てのユーザーを割り当てること'
    it ':indexテンプレートを表示すること'
  end

  describe 'Get #edit' do
    it 'リクエストは200 OKとなること'
    it '@userに要求されたユーザーを割り当てること'
    it ':editテンプレートを表示すること'
  end


  describe 'Get #show' do
    context '要求されたユーザーが存在する場合' do
      it 'リクエストは200 OKとなること'
      it '@userに要求されたユーザーを割り当てること'
      it ':showテンプレートを表示すること'
    end
    context '要求されたユーザーが存在しない場合' do
      it 'リクエストはRecordNotFoundとなること'
    end
  end

  describe 'Post #create' do
    context '有効なパラメータの場合' do
      it 'リクエストは302 リダイレクトとなること'
      it 'データベースに新しいユーザーが登録されること'
      it 'rootにリダイレクトすること'
    end
    context '無効なパラメータの場合' do
      it 'リクエストは200 OKとなること'
      it 'データベースに新しいユーザーが登録されないこと'
      it ':newテンプレートを再表示すること'
    end
  end

  describe 'Patch #update' do
    context '存在するユーザーの場合' do
      context '有効なパラメータの場合' do
        it 'リクエストは302 リダイレクトとなること'
        it 'データベースのユーザーが更新されること'
        it 'users#showにリダイレクトすること'
      end
      context '無効なパラメータの場合' do
        it 'リクエストは200 OKとなること'
        it 'データベースのユーザーは更新されないこと'
        it ':editテンプレートを再表示すること'
      end
    end
    context '要求されたユーザーが存在しない場合' do
      it 'リクエストはRecordNotFoundとなること'
    end
  end

  describe 'Delete #destroy' do
    context '存在するユーザーの場合' do
      it 'リクエストは302 リダイレクトとなること'
      it 'データベースから要求されたユーザーが削除されること'
      it 'users#indexにリダイレクトされること'
    end
    context '要求されたユーザーが存在しない場合' do
      it 'リクエストはRecordNotFoundとなること'
    end
  end

end

合ってるかは分からん。

英語で書こうとするとそれだけでちょっとしんどいので思い切って日本語で書くことにした。

describecontextの使い分けは

  • describe機能に関するアウトラインを記述
  • context特定の状態に関するアウトラインを記述

って感じらしい。全部describeでも動くと思う。

ここまででbundle exec rspec spec/controllers/users_controller_spec.rbを実行するとこんな感じ。

まだ中身書いてないからすべて黄色(pending)。

f:id:naichilab:20160116171124p:plain

すでに読みやすい。

中身を実装していく

いったん書き上げたものを載せますけど以下の点は注意。

  • 冒頭にも描いたけどbefore_action外してあるのでこのままではダメ
  • 明らかにコードの重複が多いしこのままじゃダメ
require 'rails_helper'

describe UsersController do

  describe 'Get #new' do
    before do
      get :new
    end
    it 'リクエストは200 OKとなること' do
      expect(response.status).to eq 200
    end
    it '@userに新しいユーザーを割り当てること' do
      expect(assigns(:user)).to be_a_new(User)
    end
    it ':newテンプレートを表示すること' do
      expect(response).to render_template :new
    end
  end

  describe 'Get #index' do
    before do
      @alice = create(:user, name: "alice")
      @bob = create(:user, name: "bob")
      get 'index'
    end
    it 'リクエストは200 OKとなること'  do
      expect(response.status).to eq 200
    end
    it '@usersに全てのユーザーを割り当てること' do
      expect(assigns(:users)).to match_array([@alice,@bob])
    end
    it ':indexテンプレートを表示すること' do
      expect(response).to render_template :index
    end
  end

  describe 'Get #edit' do
    before do
      @user = create(:user)
      get 'edit', permalink: @user.permalink
    end
    it 'リクエストは200 OKとなること' do
      expect(response.status).to eq 200
    end
    it '@userに要求されたユーザーを割り当てること' do
      expect(assigns(:user)).to eq @user
    end
    it ':editテンプレートを表示すること' do
      expect(response).to render_template :edit
    end
  end


  describe 'Get #show' do
    before do
      @user = create(:user)
    end

    context '要求されたユーザーが存在する場合' do
      before do
        get 'show', permalink: @user.permalink
      end
      it 'リクエストは200 OKとなること' do
        expect(response.status).to eq 200
      end
      it '@userに要求されたユーザーを割り当てること' do
        expect(assigns(:user)).to eq @user
      end
      it ':showテンプレートを表示すること' do
        expect(response).to render_template :show
      end
    end
    context '要求されたユーザーが存在しない場合' do
      it 'リクエストはRecordNotFoundとなること' do
        expect{
          get 'show', permalink: 'hogehoge'
        }.to raise_exception(ActiveRecord::RecordNotFound)
      end
    end
  end

  describe 'Post #create' do
    context '有効なパラメータの場合' do
      before do
        @user = attributes_for(:user)
      end
      it 'リクエストは302 リダイレクトとなること' do
        post :create, user: @user
        expect(response.status).to eq 302
      end
      it 'データベースに新しいユーザーが登録されること' do
        expect{
          post :create, user: @user
        }.to change(User, :count).by(1)
      end
      it 'rootにリダイレクトすること' do
        post :create, user: @user
        expect(response).to redirect_to root_path
      end
    end
    context '無効なパラメータの場合' do
      before do
        @invalid_user = attributes_for(:invalid_user)
      end
      it 'リクエストは200 OKとなること' do
        post :create, user: @invalid_user
        expect(response.status).to eq 200
      end
      it 'データベースに新しいユーザーが登録されないこと' do
        expect{
          post :create, user: @invalid_user
        }.not_to change(User, :count)
      end
      it ':newテンプレートを再表示すること' do
        post :create, user: @invalid_user
        expect(response).to render_template :new
      end
    end
  end

  describe 'Patch #update' do
    context '存在するユーザーの場合' do
      before do
        @user = create(:user)
        @originalname = @user.name
      end
      context '有効なパラメータの場合' do
        before do
          patch :update, permalink: @user.permalink, user: attributes_for(:user, name: 'hogehoge')
        end
        it 'リクエストは302 リダイレクトとなること' do
          expect(response.status).to eq 302
        end
        it 'データベースのユーザーが更新されること' do
          @user.reload
          expect(@user.name).to eq 'hogehoge'
        end
        it 'users#showにリダイレクトすること' do
          expect(response).to redirect_to user_path(assigns(:user).permalink)
        end
      end
      context '無効なパラメータの場合' do
        before do
          patch :update, permalink: @user.permalink, user: attributes_for(:user, name: '  ')
        end
        it 'リクエストは200 OKとなること' do
          expect(response.status).to eq 200
        end
        it 'データベースのユーザーは更新されないこと' do
          @user.reload
          expect(@user.name).to eq @originalname
        end
        it ':editテンプレートを再表示すること' do
          expect(response).to render_template :edit
        end
      end
    end
    context '要求されたユーザーが存在しない場合' do
      it 'リクエストはRecordNotFoundとなること' do
        expect{
          patch :update, permalink: 'hogehoge' , user: attributes_for(:user)
        }.to raise_exception(ActiveRecord::RecordNotFound)
      end
    end
  end

  describe 'Delete #destroy' do
    before do
      @user = create(:user)
    end

    context '存在するユーザーの場合' do
      it 'リクエストは302 リダイレクトとなること' do
        delete :destroy, permalink: @user.permalink
        expect(response.status).to eq 302
      end
      it 'データベースから要求されたユーザーが削除されること' do
        expect{
          delete :destroy, permalink: @user.permalink
        }.to change(User,:count).by(-1)
      end
      it 'users#indexにリダイレクトされること'do
        delete :destroy, permalink: @user.permalink
        expect(response).to redirect_to users_path
      end
    end
    context '要求されたユーザーが存在しない場合' do
      it 'リクエストはRecordNotFoundとなること' do
        expect{
          delete :destroy, permalink: 'hogehoge'
        }.to raise_exception(ActiveRecord::RecordNotFound)
      end
    end
  end

end

GETは書きやすかった。before内でページにアクセスしておいて、itで検証していく感じ。

POSTは登録できたことを確認するためにexpect{ post ~ }.to change().by(1)ってやるみたいだけど、そのために各it内でpostする形になってしまった…。 さすがにこの書き方はないと思うけどやり方がわからない。あとで調べる。

PATCHGETに似た感じで書けた。

DELETEGETは存在しないユーザーのidにhogehogeって適当な文字列与えたけどどうやるのがいいのかな?

実行結果

全部緑になった。それっぽい。

f:id:naichilab:20160119010519p:plain

とりあえず動いたけどコレジャナイ感がすごいので 本見ながらリファクタリングして続きの記事書きます。

お仕事でRuby on Rails使ってる皆さん、ツッコミお待ちしています…!

参考