.ai/principles/distilled/qa-rspec.md
RSpec.describe ClassName block..method to describe class methods and #method to describe instance methods.context to test branching logic.context blocks that differ only in their let values.Gitlab.config.gitlab.host rather than hard-coding 'localhost'.example.com or gitlab.example.com for literal URLs in tests.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') (or equivalent Capybara matcher) before calling evaluate_script or execute_script on an element to ensure it exists: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.non_existing_record_id/non_existing_record_iid/non_existing_record_access_level when you need an ID/IID/access level that doesn't actually exist — DO NOT use hardcoded values like 123 or 999.||, &&, if/else, case), verify each branch has test coverage.before_validation, before_save), ensure unit specs test the callback behavior specifically.|| operators)stub_feature_flags(flag: true) — feature flags are enabled by default in the test environment, so stubbing to true is redundant and misleading.stub_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)subject and let Variableslet definitions across contexts.let! over instance variables, let over let!, and local variables over let.let to reduce duplication throughout an entire spec file.let to define variables used by a single test — define them as local variables inside the it block.let variable inside the top-level describe block that's only used in a more deeply-nested context or describe block.let variable with another.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.using RSpec::Parameterized::TableSyntax when using table syntaxwhere blocks — use ref(:symbol) insteadbuild_stubbed or build over create when database persistence is not requiredinstance_double and spy over FactoryBot.build(...) for pure isolationlet_it_be instead of let for database-backed objects that do not change between exampleslet_it_be_with_reload when the object must be modified in a before block across exampleslet_it_be_with_refind only when a completely fresh object instance is required per example (note: incompatible with stub_method)let_it_be as immutable; use freeze: true to enforce thisallow(object).to receive(:method) stubs inside factories — use stub_method instead (factories only)stub_method outside of factories — use RSpec mocks insteadcreate_default with factory_default: :keep to share a single default object across all examples in a suite and avoid factory cascadeslet_it_be or before_all in migration specs, Rake task specs, or specs tagged :delete — use let/let! and before insteadbefore(:all) or before(:context) for common test setup — use let_it_be and before_all insteadlet_it_be blocks do not depend on a before block — let_it_be executes in before(:all) before per-example before hooks runstub_member_access_level to stub member access levels for build_stubbed objects; DO NOT use this helper when the test relies on persisted project_authorizations or Member records:js metadata to a feature spec unless the test requires JavaScript reactivity in the browserhave_no_testid instead of not_to have_testid — the latter waits the full Capybara timeout before concluding the element is absentwait: 0 to Capybara matchers inside a container already confirmed as loaded when asserting element absence; DO NOT use wait: 0 on the container itselfbuild_stubbed or build in shared examples — DO NOT use create unless the contract explicitly requires database statehave_content, have_css, have_selector, and have_link.build_stubbed instead of create in view spec setup unless the spec genuinely requires persisted state.assign to pass instance variables and allow(view).to receive(...) to stub helper methods in view specs.ActiveRecord::QueryRecorder or exceed_query_limit assertions in view specs — query performance belongs in request or controller specs.receive_message_chain in view specs.ROLE_ACTION_spec.rb (for example, user_changes_password_spec.rb).data-testid in UI tests.within with a data-testid selector only to scope interactions to a specific page areaclick_button, click_link, fill_in, select, check, choose, attach_file) — DO NOT use find(...).click or send_keys when a semantic action is availablefind_button, find_link, find_field) — use find_by_testid only when the element is not a button, link, or fieldall() 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_checked_field) — use have_css only when no specific matcher applieswithin_modal helper to interact with GitLab UI modals._('...')) in RSpec expectations against externalized contentbe_axe_clean matcher to run automated accessibility testing in feature tests.ActiveSupport::Testing::TimeHelpers (travel_to, freeze_time) for any test that exercises time-sensitive behavior:freeze_time or time_travel_to: RSpec metadata tags to reduce boilerplate for time-frozen specsQueryRecorder to assert that N+1 problems do not exist and that query counts do not increase unnoticedGitlab::GitalyClient.get_request_count to assert Gitaly request counts in a given blockspec/support/shared_*spec/support/shared_* unless they are actually shared across different bounded contextsspec/support/helpers/spec/support/helpers/ is the rootRSpec.configure in support filesrequire 'fast_spec_helper' instead of require 'spec_helper' for classes well-isolated from Railsrequire_dependency for gems not in lib/ that are needed by fast_spec_helper specsrubocop_spec_helper for RuboCop-related specsspec/factories/, named using the pluralization of their corresponding modelcreate/build in callbacks for association setupinstance method when creating factories with has_many/belongs_to associations to prevent creation of unnecessary recordsskip_callback in factories/db/post_migrate) and background migrations.require_migration! to load migration files in specs — DO NOT rely on Rails autoloadingtable helper to create temporary ActiveRecord::Base-derived models — DO NOT use FactoryBot in migration specs.migrate! helper to run the migration under test.reversible_migration helper to test migrations with change or both up and down hooks.let_it_be, let_it_be_with_reload, let_it_be_with_refind, or before_all in migration specs — use let, let!, before, or before(:all) instead.:gitlab_ci) with the appropriate migration: RSpec tag.type: :task or place specs in spec/tasks/ to automatically include RakeHelpers.run_rake_task(<task>) to execute Rake tasks in specs.:silence_stdout metadata to redirect $stdout in Rake task specs.have_gitlab_http_status over have_http_status or expect(response.status).to — it displays the response body on mismatch:ok, :no_content) over numeric codesbe_like_time or be_within when comparing timestamps from the database to Ruby Time objectsmatch_schema / match_response_schema to validate JSON responses against a schemaexpect_snowplow_event to test Snowplow tracking calls — DO NOT mock Gitlab::Tracking directlycategory argument when using expect_no_snowplow_event to avoid flaky failures from unrelated tracking callshave_no_testid instead of not_to have_testidif: Gitlab.ee? or unless: Gitlab.ee? on context/spec blocks for tests that depend on EE license: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_file_read and expect_file_read helpers to stub file contents — DO NOT stub File.read globallystub_const to modify constants in specs — DO NOT modify constants directlystub_env to modify ENV in specs.:elastic or :elastic_delete_by_query metadata; use :elastic_clean only when the other traits cause issues (it is significantly slower):prometheus tag to RSpec tests that exercise Prometheus metrics to ensure metrics are reset before each example.For the full picture, see: