.ai/principles/distilled/testing-rspec.md
spec/ subdirectory matching the source path (for example, app/models/ → spec/models/, app/services/ → spec/services/, lib/ → spec/lib/).spec/features/; name files ROLE_ACTION_spec.rb (for example, user_changes_password_spec.rb).ee/spec/ following the same structure.:js to a feature spec unless the test genuinely requires JavaScript reactivity in the browser (headless browser is significantly slower).RSpec.describe ClassName block..method to describe class methods and #method to describe instance methods.context to test branching logic (enforced by the RSpec/AvoidConditionalStatements RuboCop cop).context blocks that differ only in their let values.Gitlab.config.gitlab.host rather than hard-coding 'localhost'; use example.com / gitlab.example.com for literal URLs in tests.non_existing_record_id / non_existing_record_iid / non_existing_record_access_level when you need a non-existent ID.expect_any_instance_of or allow_any_instance_of in RSpec.:each argument to hooks — it's the default.before/after hooks scoped to :context over :all.find('.js-foo') before calling evaluate_script or execute_script on an element, to ensure the element exists.evaluate_script and execute_script as point-in-time reads that do not wait: assert a visible outcome or use wait_for before reading state with a script, and retrieve related values in a single evaluate_script call so they describe the same browser state.focus: true to isolate parts of the specs you want to run.:aggregate_failures when there is more than one expectation in a test.specify rather than it do for empty test description blocks that are self-explanatory.:eager_load tag when a test depends on all application code being loaded.subject and let Variableslet_it_be over let or let! for database-persisted objects that do not change between examples.let_it_be_with_reload when the test modifies the object and needs the database state rolled back between examples; use let_it_be_with_refind only when reload is insufficient.let to reduce duplication across an entire spec file; use local variables inside it blocks for variables used by only one test.let variable inside the top-level describe block that is only used in a nested context — keep the definition as close as possible to where it is used.let variable that's only used by the definition of another — use a helper method instead.let! only when strict evaluation with defined order is required.subject directly in examples — use a named subject subject(:name) or a let variable instead.let_it_be as immutable; freeze: true is the project default. When an example must modify one, use let_it_be_with_reload or let_it_be_with_refind so it is restored between examples.let_it_be when the factory uses stubs (allow); use let instead, or change the factory to avoid stubs.let_it_be blocks do not depend on a before block — let_it_be executes in before(:all) before per-example before hooks run.let_it_be and before_all (from test-prof) instead of before(:all) / before(:context) to share objects across examples without manual cleanup.let_it_be or before_all in migration specs, Rake task specs, or specs tagged :delete — they do not work with DatabaseCleaner's deletion strategy; use let / let! and before instead.before_all (not before) to assign roles to let_it_be objects shared across examples in a context (enforced by the RSpec/BeforeAllRoleAssignment RuboCop rule); alternatively, pass membership directly to the factory via transient attributes such as developers:, maintainers:, or owner_of:.using RSpec::Parameterized::TableSyntax when using table syntaxwhere blocks — use ref(:symbol) insteadspec/factories/, named using the pluralization of their corresponding modelcreate/build in callbacks for association setuphas_many and belongs_to associations, use the instance method to refer to the object being built (prevents unnecessary record creation via interconnected associations).skip_callback in factoriesallow(object).to receive(:method) in factories (incompatible with let_it_be); use stub_method instead, and restore with restore_original_method / restore_original_methods in after(:create).stub_member_access_level to stub member access levels for build_stubbed factory stubs; DO NOT use it when the test relies on persisted project_authorizations or Member records.after(:build) hooks); place factory specs in spec/factories_specs/.spec/fixtures/.:small_repo when the test only needs a non-empty repository (one commit, valid default branch); use :custom_repo when the test needs specific file paths or contents; use :repository only when the test depends on the full gitlab-test history (branches, tags, merge conflicts); use :empty_repo only when the test must distinguish a repository that exists but has no commits.build_stubbed > build > create; DO NOT create an object when build, build_stubbed, attributes_for, spy, or instance_double suffices — database persistence is slow.instance_double and spy instead of FactoryBot.build(...) when no real object behavior is needed.FactoryDefault (create_default) with factory_default: :keep to reuse a single object for all calls to a named factory in implicit parent associations across a suite.FDOC=1 bin/rspec (Factory Doctor) to find unnecessary database persistence; run FPROF=1 bin/rspec (Factory Profiler) to identify repetitive factory creation and cascades.bin/rspec-stackprof --speedscope=true <spec> to generate a flamegraph and identify where a slow test spends its time.:aggregate_failures when combining.allow / expect(...).to receive(...) stubs or RSpec doubles; if a unit test already verifies the output, stub it in higher-level specs.bundle exec rspec --profile -- path/to/spec.rb to identify the most expensive examples.:js, :clean_gitlab_redis_cache, :request_store, etc.) — each adds setup overhead.fast_spec_helper instead of spec_helper for classes well-isolated from Rails (skips gem loading, Rails boot, Gitaly/Shell setup); use rubocop_spec_helper for RuboCop-related specs.require_dependency '<gem>' (preferably in the library file that needs it, otherwise in the spec) when a fast_spec_helper spec exercises code that depends on a gem not located in lib/ (for example re2 via Gitlab::UntrustedRegexp).build_stubbed or build over create in shared examples.ROLE_ACTION_spec.rb; use scenario titles that describe success and failure paths.Model.count).have_content, have_current_path, have_css) and only then call model.reload — reading model state immediately after a Capybara action races the request.wait_for_requests or wait_for_all_requests in feature specs — the RSpec/AvoidWaitForRequests cop prohibits both; they wait only until no tracked request is in flight (a request can trigger another and the poll can run between the two) and they do not wait for Vue re-renders, redirects, async follow-up writes, or browser-initiated downloads..rubocop_todo/rspec/avoid_wait_for_requests.yml or disable the RSpec/AvoidWaitForRequests cop inline; replace the helper with a page-specific waiting matcher, or a narrowly targeted wait_for condition when no visible outcome exists.have_no_link / have_no_* (negative Capybara matchers) for absence checks — they return immediately when the element is absent; DO NOT use expect(page.has_link?(...)).to be(false) which waits the full timeout.have_no_testid('<id>') (not not_to have_testid) to assert a data-testid element is absent.wait: 0 only in conditional logic inside a region you have already confirmed is loaded; DO NOT use it for regular absence assertions.data-testid where possible; use within with data-testid only for scoping to a container.click_button, click_link, fill_in, select, check, choose, attach_file) rather than find(...).click.find_button, find_link, find_field) rather than generic find; use find_by_testid only when no semantic finder applies.all() with .first or block iteration to filter elements — use find() or a CSS child selector with .ancestor() instead.have_button, have_link, have_field, have_select, have_text, have_current_path, have_title) rather than generic have_css where possible.within_modal helper to interact with GitLab UI modals; use accept_gl_confirm for confirmation modals that only need to be accepted.expect(page).to have_css('.modal.show')) before interacting with controls inside it — components can disable their controls while transitioning.be_axe_clean matcher to run automated accessibility testing in feature tests._('...')) in RSpec expectations against externalized contentdisabled state to synchronize, as controls pass through transient loading states before settling.wait_for helper (from spec/support/helpers/wait_helpers.rb) only as a last resort when there is no visible UI outcome to assert on (for example, browser-initiated downloads or interactions that must be retried).polling_interval to wait_for for database-backed conditions (it polls every 0.01 seconds by default) and raise max_wait_time only when the operation genuinely needs more time.have_field, have_selector, have_content) rather than reading a value directly from an element (find(...).value, find(...).text, all(...).count) or from the session (page.current_url) — direct reads capture state at that exact moment and race asynchronous updates; assert a page-specific element before reading page.current_url.expect(find(selector).visible?).to be(true) — find already waits for a visible element, so the assertion cannot verify a later update; use a waiting matcher such as have_selector(..., exact_text: ...) for the state you need.:enable_admin_mode RSpec metadata tag to activate admin mode in specs; DO NOT use enable_admin_mode!(admin, use_ui: true) — it is slow and race-prone.perform_enqueued_jobs when testing delayed mail delivery — wrapping only the click can end the block before the AJAX request enqueues the job.:js feature spec contexts that need different setup before a page visit, keep a single sign_in and a single visit in the outermost before block and override only the relevant let in nested contexts — DO NOT call sign_in after a page has already been visited (the Warden.on_next_request injection can be consumed by a background request) and DO NOT call visit a second time on the same URL (it reloads the page and slows the spec).have_content, have_css, have_selector, and have_link; DO NOT assert on internal Ruby state or return values of view helper methods.build_stubbed instead of create in view spec setup unless the spec genuinely requires persisted state; use assign for instance variables and allow(view).to receive(...) to stub helper methods.ActiveRecord::QueryRecorder or exceed_query_limit assertions in view specs — query performance belongs in request or controller specs.receive_message_chain in view specs.type: :task or place specs in spec/tasks/ to automatically include RakeHelpers.run_rake_task('<namespace>:<task>') helper (from RakeHelpers) to execute the task under test.:silence_stdout to redirect $stdout; use :silence_output to silence both $stdout and $stderr.ActiveSupport::Testing::TimeHelpers (travel_to, freeze_time) for any test that exercises time-sensitive behaviortravel_to with explicit offsets) — equal timestamps produce non-deterministic ordering.travel_to or compute the date relative to Time.current.:freeze_time or time_travel_to: RSpec metadata tags to reduce boilerplate for time-frozen specs.reload on objects after database writes when comparing timestamps — Active Record timestamps may have higher precision than PostgreSQL's microsecond resolution, causing equality checks to fail.:clean_gitlab_redis_cache, :clean_gitlab_redis_shared_state, or :clean_gitlab_redis_queues as appropriate.:sidekiq_inline trait when a test requires Sidekiq to actually process jobs.stub_const to modify constants in specs (ensures the change is rolled back); use stub_env to modify ENV.:permit_dns to bypass universal DNS stubbing when a test genuinely requires DNS resolution.:elastic or :elastic_delete_by_query metadata; use :elastic_clean only when the other traits cause issues (it is significantly slower)stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true) to enable Elasticsearch in specs; call ensure_elasticsearch_index! after loading data to make it searchable.:prometheus tag to RSpec tests that exercise Prometheus metrics to ensure metrics are reset before each example.stub_file_read / expect_file_read helpers to stub File.read; DO NOT stub File.read globally without also calling the original for other paths.path override on :legacy_storage projects — the default path includes the project ID and avoids repository conflicts between specs.:disable_rate_limit when a single test triggers rate limiting; use :clean_gitlab_redis_rate_limiting when rate limiting is triggered across multiple examples in a feature spec using :js.:verify_workhorse_jwt only when the test asserts the Workhorse JWT enforcement boundary itself (for example, a 403 Forbidden response when the header is absent); DO NOT apply it to ordinary tests — auto-injection covers them.have_gitlab_http_status (with named status symbols like :ok, :no_content) instead of have_http_status or expect(response.status).to — it shows the response body on mismatch.be_like_time or be_within when comparing timestamps from the database to Ruby Time objects (precision differs between OS and PostgreSQL).match_schema / match_response_schema to validate JSON responses against a JSON schema.be_valid_json to validate that a string parses as JSON; combine with match_schema using .and.expect_snowplow_event to assert legacy Snowplow tracking calls (catches runtime type-check errors); use expect_no_snowplow_event with at least a category argument to avoid flakiness.match_snowplow_context_schema to validate Snowplow context against a JSON schema in spec/fixtures/product_intelligence/.:js) specs — put QueryRecorder and exceed_query_limit assertions in request or controller specs; prime the baseline with a warmup request when the first call lazily loads caches.Gitlab::GitalyClient.get_request_count to assert the number of Gitaly requests made by a block of code.spec/support/shared_*.config.include calls in spec/spec_helper.rb or scope them with type modifiers.spec/support/helpers/, following Rails naming conventions (spec/support/helpers/ is the root).spec/support/, one file per domain.scripts/rspec_check_order_dependence spec/path/to/spec.rb and remove the file from rspec_order_todo.yml once it passes.rspec --bisect to identify order-dependent failures.||, &&, if/else, case), verify each branch has test coveragebefore_validation, before_save), ensure unit specs test the callback behavior specifically|| operators)default_enabled: value in the YAML definition), so DO NOT stub a flag to true to reach its enabled path — the only exception is a flag explicitly disabled in spec/spec_helper.rb, where stubbing to true is warrantedstub_feature_flags(flag: false) to test the disabled code pathspec/requests/api/pages_spec.rb, DO NOT create spec/requests/api/pages/pages_spec.rb)match_array instead of eq or match to avoid dataset-specific flakiness.first, .last, or .take on an ActiveRecord relation without an explicit .order(...) — PostgreSQL does not guarantee row order without ORDER BY, which causes dataset-specific flaky testsFaker or random values, ensure the test handles any value the generator can produce; if the test depends on a specific format, use a hardcoded value insteadFor the full picture, see: