This PR updates the RSpec agent skill with Discourse-specific guidance for choosing assertions, structuring fixtures, and writing reliable system tests. The copied generic RSpec style guide duplicated upstream advice that could drift from both its source and Discourse's conventions. Key changes: * Add public-boundary and test-layer guidance so specs assert behavior owned by the layer under test. * Add fabricator, system-test cost, deterministic transient-state, URL, and page-object guidance to reduce brittle tests. * Remove the copied style guide and its references so the skill only maintains Discourse-specific advice.
3.2 KiB
Vendored
Request Specs
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.
File and Top-Level Naming
- File:
spec/requests/<resource>_controller_spec.rb— name after the controller it covers (e.g.bookmarks_controller_spec.rbforBookmarksController). - Top-level:
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
Group by Controller Action
One describe block per controller action, named "#action_name":
describe "#index" do
end
describe "#create" do
end
describe "#destroy" do
end
The # prefix identifies an instance method. 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.
Scenarios Within an 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.
Authentication
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.
What to Assert
Request specs verify the observable HTTP behavior and any externally visible side effects:
- Response status:
expect(response.status).to eq(200) - Response body:
expect(response.parsed_body["errors"]).to include(...)— useparsed_bodyfor JSON - Persisted state:
expect(Bookmark.find_by(id: bookmark.id)).to eq(nil)— direct DB checks are fine here, since the controller's job is to mutate state - Emitted events / enqueued jobs when relevant
Don'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.
Issuing Requests
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.