.skills/discourse-writing-rspec-tests/references/request-specs.md
Discourse uses request specs (spec/requests/) rather than controller specs to test controllers. They drive real HTTP requests against the Rails router, so they verify routing, middleware, controller actions, and response payloads end-to-end.
spec/requests/<resource>_controller_spec.rb — name after the controller it covers (e.g. bookmarks_controller_spec.rb for BookmarksController).RSpec.describe SomeController do — reference the controller class directly, not a string.# frozen_string_literal: true
RSpec.describe BookmarksController do
fab!(:user)
describe "#create" do
# ...
end
end
One describe block per controller action, named "#action_name":
describe "#index" do
end
describe "#create" do
end
describe "#destroy" do
end
The # prefix follows the instance-method convention from the RSpec style guide. Each action's describe block is the home for all scenarios that hit that action — signed-in vs anonymous, permission variations, parameter variations, etc.
Reserve bare descriptive strings (describe "extensibility event") for cross-cutting concerns that don't map to a single action.
Use context blocks for scenarios within an action — pair positive and negative cases:
describe "#create" do
before { sign_in(user) }
it "creates the bookmark" do
post "/bookmarks.json", params: { bookmarkable_id: post.id, bookmarkable_type: "Post" }
expect(response.status).to eq(200)
end
context "when the user has reached the bookmark limit" do
before { SiteSetting.max_bookmarks_per_user = 1 }
it "returns a 400 with an explanatory error" do
# ...
end
end
end
Keep nesting to 2 levels max (per the top-level testing principles). If a scenario needs more depth, flatten by encoding it into the it description.
Sign in inside the action's describe block (or a context), not at the top of the file — different actions often have different auth requirements:
describe "#create" do
before { sign_in(user) }
# ...
end
For anonymous-user scenarios, omit sign_in and assert the expected redirect or 403.
Request specs verify the observable HTTP behavior and any externally visible side effects:
expect(response.status).to eq(200)expect(response.parsed_body["errors"]).to include(...) — use parsed_body for JSONexpect(Bookmark.find_by(id: bookmark.id)).to eq(nil) — direct DB checks are fine here, since the controller's job is to mutate stateDon't assert on internal method calls (Controller.any_instance.expects(:foo)) — that couples the test to implementation. If the response and state are correct, the implementation is correct.
Make real HTTP calls — don't stub the controller:
get "/bookmarks.json"
post "/bookmarks.json", params: { ... }
put "/bookmarks/#{id}.json", params: { ... }
delete "/bookmarks/#{id}.json"
Use the .json suffix for JSON endpoints; omit it for HTML endpoints.