.skills/discourse-writing-rspec-tests/SKILL.md
Discourse uses RSpec for testing. Follow these patterns for all test types.
describe ".call", describe "#execute", or describe "#expire!").it block verifies one behavior for clear failure diagnosis.SomeService.expects(:some_method).never (or .once, .with(...)) couple the test to internal implementation details that the caller shouldn't care about. Assert on the observable outcome instead: returned value, persisted state, emitted event, response body, rendered output. If the implementation is later refactored, inlined, or renamed, a behavior-focused test still passes when the behavior is correct.DiscourseEvent.track_events and MessageBus.track_publish when asserting emitted events or published messages instead of expecting calls to trigger or publish.shared_examples/let chains that hurt readability.fab! for records shared across examples, let for lazy per-example values, let! when a per-example record must exist before the action, and subject for the operation under test. Use inline Fabricate for a record local to one example. Because fab! uses TestProf let_it_be, it cannot depend on let or other per-example setup. Keep let value-oriented rather than hiding a parameterized helper in a Proc or lambda. When parameterized behavior needs a name to keep one spec readable, define a small method in that example group; do not introduce one merely to wrap a simple Fabricate, create!, or direct call. Move the method into a focused, auto-loaded spec/support module only when it is shared by multiple spec files. Search spec/fabricators before creating setup by hand, and define a derived fabricator for a recurring record shape.describe/context block to confirm the test belongs there. Check that the parent context's description, let/fab! setup, and before hooks match the scenario being tested. A misplaced test inherits the wrong setup and produces misleading results.describe "after_commit :callback_name".|vote|, |option|, not |v|, |o|.contain_exactly or eq instead of multiple include/not_to include checks.expect(response.body).not_to include("hidden data explorer excerpt") silently passes if the literal has a typo or drifts from the source, giving a false sense of security. Reference the object directly (expect(response.body).not_to include(private_post.raw)) so the assertion stays in sync with the data under test. The same applies to any not_to include/not_to match against hardcoded strings.describe/context nesting. Instead of deeply nested contexts, put the full scenario description in the it block itself. Flat tests are easier to read and maintain."returns true when topic_approval_type is approval or pre_approval" over "returns true when topic_approval_type is not none". Be specific about the values being tested.Tests have setup overhead. Optimize for the fewest test examples possible:
it block when testing the same page/stateit block incurs setup overhead; batch checks where logical# Run specific file
bin/rspec spec/models/topic_spec.rb
# Run specific line
bin/rspec spec/models/topic_spec.rb:15
describe grouping, and what to assert.See references/tracking-helpers.md for DiscourseEvent.track_events, MessageBus.track_publish, and track_sql_queries — block helpers that capture events, message-bus publishes, and SQL queries so tests can assert on side effects.