.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.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; use freeze: true to enforce immutability and detect state leakage.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.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/.:repository trait on project factories to get a copy of the gitlab-test repository; prefer :custom_repo when you need to specify exact file contents.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 (it does not account for Vue re-render, redirect completion, async follow-up writes, or browser-initiated downloads).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.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); DO NOT use the deprecated wait_for_requests in new specs.have_field, have_selector, have_content) rather than reading a value directly from an element (find(...).value, find(...).text, all(...).count) — direct reads capture state at that exact moment and race asynchronous updates.: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.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.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 path.spec/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: