15. turnip による受入テスト(1)

以前、受入テストは RSpec とは別に Cucumber というアプリケーションを使っていた。最近は、Turnip と Capybara を用いて、RSpec 内で実施できる受入テストが使われている。今回は、年度に関するテストで受入テストを実施してみる。

turnip の設定

すでに Gemfile には登録してあるので、その後の設定を行う。まず、.rspec に以下の行を追加する。.rspec は .gitignore に入れてあるので、各マシンで設定を追加しておく。Jenkins の設定も忘れないこと。

-r turnip/rspec

次に、spec/features, spec/steps のフォルダを作成しておく。

$ mkdir -p spec/acceptance/{features,steps}

spec/acceptance/steps の読み込みを行うために、spec/turnip_helper.rb を作成する。また、最後の二行は Capybara でログイン処理をスキップするため(login_as を呼び出すため)に必要なものである。

Dir.glob("spec/acceptance/steps/*steps.rb") { |f| load f, true }

include Warden::Test::Helpers
Warden.test_mode!

feature の記述

さっそく、spec/acceptance/features/kyoumu_years.feature を書いてみる。本システムではリンクをわかりやすくするためにすべてを括っている。ただし、 が入っていると turnip がプレースホルダーを認識しないので、”” で括っておく。ここでは途中までをテストする。続きは後日。

# language: ja
機能: 年度の追加、変更
  シナリオ: 教務室のトップページから年度変更に飛び、新規年度を作成し、年度を変更する
    前提 教務室でログインする
    ならば "[年度変更]"と表示されていること
    もし "[年度変更]"リンクをクリックする
    ならば 画面に年度一覧と表示されていること

Guard でリターンキーを押すと次のように表示される。「教務室でログインする」という step がないので PENDING になっている。

年度の追加、変更
  教務室のトップページから年度変更に飛び、新規年度を作成し、年度を変更する
    前提教務室でログインする -> ならば”[年度変更]”と表示されていること -> もし”[年度変更]”リンクをクリックする -> ならば年度一覧と表示されていること (PENDING: No such step: '教務室でログインする')

認証のスキップ

このテストを通過させるために、「教務室でログインする」機能を step として記述する。今後いくつかの権限のものを追加する予定なので、プレイスホルダーを使って切り替えられるようにしておく。spec/acceptance/steps/login_steps.rb は以下の記述した。

USERS = [
  ['教務室', :user_skyoumu ],
  ['教員', :user_hkob ],
]

step %(:jnameでログインする) do |jname|
  login_as(FactoryGirl.create(USERS.assoc(jname).last), scope: :user)
  visit root_path
end

RSpec で使用した login_user_as は受入テストでは使用できないので、login_as を使用している。また、jname は日本語で与えられるので、USERS 配列(後で増やす予定)から assoc で FactoryGirl のキーを取り出している。

step を保存すると Guard が起動し、以下の様なエラーとなる。

  1) 年度の追加、変更 教務室のトップページから年度変更に飛び、新規年度を作成し、年度を変更する 前提教務室でログインする -> ならば”[年度変更]”と表示されていること -> もし”[年度変更]”リンクをクリックする -> ならば年度一覧と表示されていること
     Failure/Error: 前提 教務室でログインする
     ArgumentError:
       Factory not registered: user_skyoumu

これは user_skyoumu という Factory がないということなので、spec/factories/user.rb に前回同様継承ファクトリで作成する。

    factory :user_skyoumu do
      name 'skyoumu'
    end

この結果、認証はスキップされ、welcome#index に飛ぶことができた。

     Failure/Error: ならば “[年度変更]”と表示されていること
       expected to find text “[年度変更]in "Welcome#index Find me in app/views/welcome/index.html.haml ログアウト"

権限の確認 (User モデル)

教務室でログインした場合、welcome#index から教務室用のトップページにリダイレクトして欲しい。旧システムではコントローラでかなり頑張ってしまったので、ユーザ側で判断をするように変更したい。feature のテストの途中だが、welcome_controller の spec を修正する。後で別の権限のテストも行うので、user_skyoumu 用の context を用意し、teacher_kyoumus_path へのリダイレクト処理に変更している。旧システムより教職員用サーバの URL はすべて teacher/ 以下の namespace を利用している。後で学生がこの URL にはアクセスできないように権限設定をするためである。

require 'rails_helper'

RSpec.describe WelcomeController, :type => :controller do
  describe "GET index" do
    context "for :user_skyoumu" do
      it do
        login_user_as :user_skyoumu
        get :index
        expect(response).to redirect_to(teacher_kyoumus_path)
      end
    end
  end
end

Guard の画面には以下のようにエラーが表示される。

  1) WelcomeController GET index for :user_skyoumu 
     Failure/Error: expect(response).to redirect_to(teacher_kyoumus_path)
     NameError:
       undefined local variable or method `teacher_kyoumus_path' for #<RSpec::ExampleGroups::WelcomeController::GETIndex::ForUserSkyoumu:0x007fec099ceee0>

teacher_kyoumus_path のための generator を実行する。

$ bin/rails g controller teacher/kyoumus index
      create  app/controllers/teacher/kyoumus_controller.rb
       route  namespace :teacher do
  get 'kyoumus/index'
  end
      invoke  haml
      create    app/views/teacher/kyoumus
      create    app/views/teacher/kyoumus/index.html.haml
      invoke  rspec
      create    spec/controllers/teacher/kyoumus_controller_spec.rb
      invoke  assets
      invoke    coffee
      invoke    scss

config/routes.rb を見ると以下のように追加されている。

  namespace :teacher do
  get 'kyoumus/index'
  end

このままだと teacher_kyoumus_index_path になってしまうので、resource に修正する。

  namespace :teacher do
    resources :kyoumus, only: [ :index ]
  end

この結果、Guard 画面は redirect されていないというエラーに変わる。

  2) WelcomeController GET index for :user_skyoumu should redirect to "/teacher/kyoumus"
     Failure/Error: expect(response).to redirect_to(teacher_kyoumus_path)
       Expected response to be a <redirect>, but was <200>
     # ./spec/controllers/welcome_controller_spec.rb:9:in `block (4 levels) in <top (required)>'

このリダイレクトを実現するために、app/controllers/welcome_controller.rb の index メソッドを以下のように修正する。user でログインしているときには、current_user に user_session を渡して、権限を設定してもらう。その後、権限を確認するメソッド is_kyoumu? を用いてリダイレクトを行う(この部分も後日増える予定)。

  def index
    if user_signed_in?
      current_user.set_privileges(user_session)
      redirect_to teacher_kyoumus_path if is_kyoumu?
    end                                  
  end 

set_privileges メソッドがないと言われる。

  1) WelcomeController GET index for :user_skyoumu 
     Failure/Error: get :index
     NoMethodError:
       undefined method `set_privileges' for #<User:0x007fcf311fee30>

set_privileges を作成するため、先にテストを記述する。spec/models/user_spec.rb を以下のように記述した。%i はシンボルの配列を作成するもので、Ruby 2.0 からの新しい記述法である。また、values_at は以前は indexes と呼ばれていたもの。session ハッシュの User.Privileges に対応する値を一括で取り出し、answers と比較する。User.Privileges 配列を引数として展開するため * を使っている。ユーザや権限はこれから増えてくるのでそれに耐えうるテストにしている。

RSpec.describe User, :type => :model do
  context "Each user of users" do
    user_keys = %i( user_hkob user_skyoumu )

    context "checking the privileges" do
      answers = {                    
        user_hkob: [ false ],
        user_skyoumu: [ true ],
      }   
      user_keys.each do |user_key|
        user = FactoryGirl.build(user_key)
        session = {}
        user.set_privileges(session)
        it { expect(session.values_at(*User.Privileges)).to eq(answers[user_key]) }
      end
    end
  end

この結果「undefined method `set_privileges' for # (NoMethodError)」というエラーになる。そこで、
app/models/user.rb に set_privileges を記述する。また、Privileges 定数も定義しておく。Privileges 配列に対応するメソッドを呼び出し、true であった場合にハッシュに登録する。Privileges も当初 %i で作成していたが、session にはシンボルで代入しても取り出すときには文字列になってしまうことがわかり、後から %w に修正した。

  # 権限一覧
  Privileges = %w( is_kyoumu? )

  # @param [Hash] session ハッシュ
  # @return [Hash] 更新された session ハッシュ
  def set_privileges(session)
    Privileges.each do |p|
      session[p] = true if self.send(p)
    end
    session
  end

こんどは、「undefined method `is_kyoumu?' for # (NoMethodError)」というエラーになる。そこで、app/models/user.rb に is_kyoumu? メソッドを作成する。name が skyoumu または akyoumu であれば、true を返せばよいので、以下のようになる。

  def is_kyoumu?
    %w( skyoumu akyoumu ).include?(name)
  end

この結果、set_privileges のテストは以下のように通過する。

User
  Each user of users
    checking the privileges
      should eq [nil]
      should eq [true]

ただし、仕様のメッセージが悲しいので、it に文章を記述する。

        it "#{user_key}'s privileges for #{User::Privileges} should eq #{answers[user_key]}" do
          expect(session.values_at(*User::Privileges)).to eq(answers[user_key])
        end 

この結果、RSpec の表示は以下のようになる。

User
  Each user of users
    checking the privileges
      user_hkob's privileges for [“is_kyoumu?”] should eq [nil]
      user_skyoumu's privileges for [“is_kyoumu?”] should eq [true]

モデルのテストが通過したので、Guard でリターンキーを押してみる。モデルの実装が終わったので、今度は feature のテストが進んでいる。

  1) 年度の追加、変更 教務室のトップページから年度変更に飛び、新規年度を作成し、年度を変更する 前提教務室でログインする -> ならば”[年度変更]”と表示されていること -> もし”[年度変更]”リンクをクリックする -> ならば年度一覧と表示されていること
     Failure/Error: 前提 教務室でログインする
     NoMethodError:
       undefined method `is_kyoumu?' for #<WelcomeController:0x007fea92039df8>

これは、controller の is_kyoumu? がないというエラーである。これはすべての controller から呼ばれるため、app/controllers/applications_controller.rb に記載する。admin でも同様の権限判断が入る予定なので、user_session だけでなく admin_session も確認するようにしている。セッションキーを確認する each_session は引数がある場合は、その引数の値を返し、ない場合には session 自体を返す。

  # @param [Symbol] key 確認する session キー(省略可)
  # @return [Hash] key がないとき admin_session か user_session
  # @return [Object] key があるとき admin_session か user_session のどちらかの session キーの内容
  def each_session(key = nil)                     
    ans = user_session || admin_session           
    ans = ans && ans[key] if key                        
    ans                          
  end

  # @return [Boolean] session[is_kyoumu?] を返す(@is_kyoumu にキャッシュする)
  def is_kyoumu?
    @is_kyoumu ||= each_session('is_kyoumu?')
  end

この結果、feature の結果は以下のようになった。正しく kyoumus/index にリダイレクトされるようになった。

  1) 年度の追加、変更 教務室のトップページから年度変更に飛び、新規年度を作成し、年度を変更する 前提教務室でログインする -> ならば”[年度変更]”と表示されていること -> もし”[年度変更]”をクリックする -> ならば年度一覧と表示されていること
     Failure/Error: ならば “[年度変更]”と表示されていること
       expected to find text “[年度変更]” in "Teacher::Kyoumus#index Find me in app/views/teacher/kyoumus/index.html.haml"

かなり長くなったので、本日はここまで。

written by iHatenaSync