1

I'm wondering if there are better ways to write controller request specs than how I currently do it. I'm using the devise gem for authentication. This is how I'd test an admin controller:

  describe "#index" do
    context "when not logged in" do
      it "redirects to root page" do
        get admin_index_path

        expect(response).to redirect_to root_path
      end
    end

    context "when logged in as an user" do
      before { sign_in user }

      it "redirects to root page" do
        get admin_index_path

        expect(response).to redirect_to root_path
      end
    end

    context "when logged in as an admin" do
      before { sign_in admin }

      it "opens the page" do
        get admin_index_path
        expect(response).to be_success
      end
    end
  end

As you can see there is some "boilerplate" code which repeats over many of my controllers. For controllers which require a user to be logged in, I'd had to write a "not logged in" spec for every controller action. How do you do this? Is there a way to maybe shorten/share the code between the specs? The only thing that changes is the path.

Linus
  • 4,643
  • 8
  • 49
  • 74

2 Answers2

4

@Linus here is refactored version of your answer

shared_examples "requires login" do |path, user_type|
  context "when not logged in" do
    it "redirects to root path" do
      get public_send("#{path}_path")

      expect(response).to redirect_to root_path
    end
  end

  context "as an #{user_type}" do
    it "redirects to root path" do
      sign_in create(user_type)

      get public_send("#{path}_path")

      expect(response).to redirect_to root_path
    end
  end
end

And use it like

it_behaves_like "requires login", "admin_index", :user for user

it_behaves_like "requires login", "admin_index", :admin for admin

Amit Patel
  • 15,609
  • 18
  • 68
  • 106
  • Nice idea to pass the the user type as a symbol, which is just like factory bot needs it. Thanks! – Linus Nov 22 '17 at 10:50
  • How would you pass a path with an object? Example: `edit_course_path(course)` – Linus Nov 22 '17 at 11:01
  • `edit_course_path(course)` returns string which you can pass as argument like `it_behaves_like "requires login", edit_course_path(course), :admin` – Amit Patel Nov 22 '17 at 11:37
  • But I can only write `edit_course_path(course)` in `it` blocks, right? – Linus Nov 23 '17 at 09:30
  • I didn't quite get you @Linus. Can you please add more details? – Amit Patel Nov 23 '17 at 12:57
  • Sorry I meant "outside of it blocks". As `it_behaves_like` isn't written inside of `it` blocks, but only inside `describe`/`context` blocks, it throws an error, as path evaluations, like `edit_course_path(course)`, is only available inside of `it` blocks. – Linus Nov 23 '17 at 13:18
0

Ok, I came up with this solution. Please let me know if you have any better ideas.

shared_examples "requires user login" do |path|
  context "when not logged in" do
    it "redirects to root path" do
      get public_send(path)

      expect(response).to redirect_to root_path
    end
  end

  context "as an user" do
    it "redirects to root path" do
      sign_in create(:user)

      get public_send(path)

      expect(response).to redirect_to root_path
    end
  end
end

shared_examples "requires admin login" do |path|
  context "as an user" do
    it "redirects to root path" do
      sign_in create(:user)

      get public_send(path)

      expect(response).to redirect_to root_path
    end
  end

  context "as an admin" do
    it "gets 200" do
      sign_in create(:admin)

      get public_send(path)

      expect(response).to be_success
    end
  end
end

To use them:

it_behaves_like "requires user login", "admin_index_path"

or

it_behaves_like "requires admin login", "admin_index_path"
Linus
  • 4,643
  • 8
  • 49
  • 74