cancancanのテストをRSpecで書く


cancancanを使った際、テストに学びがあったので投稿します。

前提:cancancanとは

権限による制御が簡単にできるgemです。

cancancanのテスト

requestスペックなどで特定の権限が各アクションを実行できるかどうかをテストする方法もあると思いますが、今回はcancancanが用意しているmatcherを使ってテストする方法を紹介します。

ability_spec.rb
require "cancan/matchers"

describe "User" do
  describe "abilities" do
    subject(:ability) { Ability.new(user) }
    let(:user) { nil }

    context "when is an account manager" do
      let(:user) { create(:account_manager) }

      it { is_expected.to be_able_to(:manage, Account.new) } 
    end
  end
end

コード例は、上記ドキュメントに書いてあるままです。
ポイントは、cancan/matchersをrequireすることでbe_able_toというmatcherを使えるようになるというところだと思います。

注意点

複数の権限をテストするときは注意が必要です。
例えば、下記のようなかたちで権限を設定しているとします。

ability.rb
def initialize(user)
  can :read, Profile
  can :read, Job
  cannot :read, Account
end

この時、RSpecを以下のようなかたちにすると、テストが通ります。

ability_spec.rb

let(:user) { create(:user) }

it { is_expected.not_to be_able_to(:read, [Profile.new, Account.new]) }  # => テスト通る

この結果は、私の直感に反していました。
Profileにはcan: readを設定しているので、テストは失敗するのではないかと思ったからです。
一方で、以下のテストは失敗します。

ability_spec.rb
let(:user) { create(:user) }

it { is_expected.to be_able_to(:read, [Profile.new, Job.new]) } # => テスト通らない

どちらもreadにはcanを設定しているのですが、失敗してしまいます。

どうやらbe_able_toの第2引数には配列を渡してもまとめてチェックしてくれるわけではないみたいです。
(権限設定側では、can :read, [Profile, Job]のかたちでまとめて権限付与できます)

したがって、最適解かどうかはわかりませんが、aggregate_failuresなどを使って1行ずつ書いていくといった方法が良いのかなと思いました。

ability_spec.rb
let(:user) { create(:user) }

it 'テスト内容' do
  aggregate_failures do
   is_expected.to be_able_to(:read, Profile.new)
   is_expected.to be_able_to(:read, Job.new)
  end
end

ちなみに、be_able_toの第1引数はまとめて記載できます。

abitliy.rb
def initialize(user)
  can [:create, :read], Profile
end

上記の設定をしているとき、以下のテストはコメントに書いてある通りの結果になるので、第1引数は配列のかたちでまとめて書いてしまって問題なさそうです。

ability_spec.rb
let(:user) { create(:user) }

it { is_expected.to be_able_to([:create, :read], Profile.new) } # => テスト通る
it { is_expected.to be_able_to([:create, :update], Profile.new) } # => テスト通らない(updateはcanの設定をしていないので期待通り)

間違いなどあったらご指摘ください。