facade-test-conventions

star 1.8k

Conventions for writing and reviewing unit and integration tests for Git::Repository facade methods (modules under lib/git/repository/). Use when scaffolding new facade tests or auditing existing ones in spec/unit/git/repository/ and spec/integration/git/repository/.

ruby-git By ruby-git schedule Updated 6/5/2026

name: facade-test-conventions description: "Conventions for writing and reviewing unit and integration tests for Git::Repository facade methods (modules under lib/git/repository/). Use when scaffolding new facade tests or auditing existing ones in spec/unit/git/repository/ and spec/integration/git/repository/."

Facade Test Conventions

Conventions for writing and reviewing unit and integration tests for facade methods on Git::Repository::* modules.

Contents

How to use this skill

Attach this file to your Copilot Chat context, then invoke with the spec file(s) to write or review. Include the corresponding facade module for context. Examples:

Using the Facade Test Conventions skill, scaffold tests for Git::Repository::Staging.
Facade Test Conventions review: spec/unit/git/repository/committing_spec.rb.

Related skills

Input

The invocation needs the unit and/or integration spec file(s) to review. Including the corresponding facade module file (lib/git/repository/<topic>.rb) provides useful context for verifying delegation contracts and option forwarding.

Prerequisite: Read the entire RSpec Unit Testing Standards skill (line 1 through EOF) before beginning. It defines the baseline Rules 1–28 that this skill extends.

Reference

Unit tests

Facade unit tests verify the orchestration contract between the facade method and the components it calls (Git::Commands::*, Git::Parsers::*, Git::ExecutionContext::Repository). They do not run real git.

The collaborators (commands, parsers) are stubbed via instance_double. The unit test asserts:

  1. The facade constructs each command class with the injected @execution_context.
  2. Each command's #call is invoked with the expected positional and keyword arguments (verifying argument pre-processing).
  3. For multi-command sequences, the calls happen in the documented order.
  4. The parser/result-class is invoked with the command's stdout (when applicable).
  5. The facade returns the value its public contract documents.

Setup pattern

RSpec.describe Git::Repository::Staging do
  let(:execution_context) { instance_double(Git::ExecutionContext::Repository) }
  let(:described_instance) { Git::Repository.new(execution_context: execution_context) }
  let(:command_result) { instance_double(Git::CommandLineResult, stdout: '') }
  let(:add_command) { instance_double(Git::Commands::Add) }
  let(:add_result) { command_result }

  before do
    allow(Git::Commands::Add).to receive(:new).with(execution_context).and_return(add_command)
  end

  describe '#add' do
    # ...
  end
end

The shared command_result let provides a default empty-stdout result; each per-command alias (add_result, branch_list_result, ...) lets individual tests override stdout in isolation — e.g. let(:add_result) { instance_double(Git::CommandLineResult, stdout: 'fixture output') } in a nested context — without affecting other tests in the file.

Setup invariants:

  • The subject is an instance of Git::Repository, not the module itself. Modules are mixed into the class; tests must exercise the class to reflect real call sites.
  • execution_context is an instance_double(Git::ExecutionContext::Repository) — never a double('ExecutionContext') and never a real context.
  • Each command class is stubbed with allow(Klass).to receive(:new).with(execution_context).and_return(...) so the facade's command construction (with the right execution context) is verified by the stub.

Cover these cases

  • Default invocation — facade called with no arguments (or only required positional args) delegates with the documented defaults. Assert the return value once per facade method to verify pass-through.
  • Each positional argument variation — single value, array, nil where applicable.
  • Each option the facade exposes — including aliases, deprecated keys, and policy defaults the facade applies (no_edit: true, etc.).
  • Multi-command sequences — when the facade calls more than one command, use expect ... receive(:call).with(...).ordered to assert ordering and intermediate-result wiring.
  • Parser invocation — when the facade uses a Git::Parsers::* class, stub the parser and assert it is called with the command's stdout. Assert the facade returns what the parser returned.
  • Raw CommandLineResult return — when the facade's contract is to return the command's Git::CommandLineResult directly (not .stdout and not a parser output), assert eq(<command>_result) to verify pass-through.
  • Option whitelisting — when the facade defines a <METHOD>_ALLOWED_OPTS constant and calls Git::Repository::Internal.assert_valid_opts!, test that an unknown key raises ArgumentError and a known key is forwarded.
  • Deprecation handling — when the facade rewrites or warns on deprecated keys, test that the deprecation warning is emitted and the new key is forwarded.
  • Signature compatibility call shapes — when a facade method preserves a legacy public contract, include tests for each call shape the 4.x public API used (positional hash and/or keyword-arg / **opts where applicable).

Expectations for command invocation

Use the standard rspec-mocks form (no command-specific helper exists for the facade layer):

it 'delegates to Git::Commands::Add#call with the given path' do
  expect(add_command).to receive(:call).with('path/to/file.rb').and_return(add_result)
  described_instance.add('path/to/file.rb')
end

For command + parser orchestration (single command whose stdout is fed to a parser), use .ordered to assert the call sequence:

it 'lists branches then parses the output' do
  expect(branch_list_command).to(
    receive(:call)
        .with(all: true, format: Git::Parsers::Branch::FORMAT_STRING)
        .and_return(branch_list_result)
        .ordered
  )

  expect(Git::Parsers::Branch).to(
    receive(:parse_list)
        .with(branch_list_result.stdout)
        .and_return(parsed_branches)
        .ordered
  )

  expect(described_instance.branches_all).to eq(parsed_branches)
end

For genuinely multi-command orchestration (the facade calls more than one command), chain .ordered across each command's #call, wiring intermediate results through as needed:

it 'saves the stash then lists stashes' do
  expect(stash_save_command).to(
    receive(:call).with(message: 'wip').and_return(stash_save_result).ordered
  )

  expect(stash_list_command).to(
    receive(:call).and_return(stash_list_result).ordered
  )

  expect(described_instance.stash_save_and_list(message: 'wip')).to eq(parsed_stashes)
end

What not to test

  • Command argv building. That is the command class's contract and is covered by spec/unit/git/commands/<command>_spec.rb. The facade unit test should stub #call and assert the keyword arguments the facade passes — not assert on the CLI tokens that reach git.
  • Parser internals. Stub the parser class method and assert the facade calls it with the right input. Parser parsing is covered by spec/unit/git/parsers/.
  • Real command execution. Facade unit tests must not exercise Git::ExecutionContext::Repository for real. Use instance_double.
  • Multiple input strings exercising the same code path — one test per argument type is sufficient (string vs. array vs. nil), not one per value.
  • #initialize of the facade module. The module is mixed into Git::Repository; constructor coverage belongs to repository_spec.rb.

Unit test grouping

One describe '#<method_name>' block per facade method. Inside, use flat context blocks per argument variation. Optional sections at the end (in order) when present:

  • context 'option whitelisting'Git::Repository::Internal.assert_valid_opts! raises on unknown keys and forwards known keys unchanged (no slice — the assertion is the only enforcement mechanism)
  • context 'deprecation handling'Git::Deprecation.warn assertions and key-rewrite tests
  • context 'input validation'ArgumentError raised by the facade itself (not by the command)
  • context 'signature compatibility' — for legacy-contract methods, exercises required call shapes (legacy positional hash and/or keyword-arg / **opts forms)

The exit code section that command specs use does not apply to facade specs — exit-status handling is the command's concern; the facade's tests assume the command either returns a result or raises.

Integration tests

Facade integration tests run real git in a temp repository and verify the end-to-end Ruby return value of the facade method.

Each integration spec file tests one facade module (one spec/integration/git/repository/<topic>_spec.rb). Inside, group by facade method.

When to write integration tests

Facade integration tests are the exception, not the default. Most facade behavior is already covered end-to-end by the underlying command's own integration tests; re-running real git through the facade re-exercises the same code path without adding signal.

Write a facade integration test only when the facade adds behavior that is not exercised by any single command's integration tests:

  • Multi-command orchestration — the facade calls more than one command and the integration test confirms the documented end-to-end value emerges from the sequence against real git.
  • Facade-owned post-processing of real git output — the facade itself (not the command) parses, aggregates, or transforms raw command output before returning. A real git invocation proves the post-processing handles actual output rather than a mocked string.

When to skip integration tests

Skip for everything else, including:

  • One-line delegators that pass arguments through to a single command with no pre/post-processing (e.g. Git::Repository::Staging#add, #reset).
  • Single-command facade methods that delegate parsing to a parser or result-class factory — the command's own integration test already exercises that command + parser against real git.
  • Argument pre-processing (path normalization, deprecation key rewrites, option whitelisting) — these are pure-Ruby transforms with no git involvement; unit tests prove them and real git adds no signal.
  • Error-path assertions (raise_error(Git::FailedError)) — these test the command's error wrapping, not the facade.

When skipping, document why with a code comment in the spec file or a # header in spec/integration/git/repository/<topic>_spec.rb explaining which methods are covered exclusively by command integration tests.

Integration test grouping

Mirror the Command Test Conventions integration grouping. Use a multi-command or post-processing facade method — single-command delegators do not warrant integration tests (see When to skip integration tests):

The shared context (e.g. 'in an empty repository') provides repo and repo_dir helpers. Facade integration specs must override execution_context to a Git::ExecutionContext::Repository (the shared context's default is repo.lib, a Git::Lib). Stage any required repository state in a before block inside the spec itself.

RSpec.describe Git::Repository::Stashing, :integration do
  include_context 'in an empty repository' # provides repo and repo_dir helpers

  let(:execution_context) { Git::ExecutionContext::Repository.from_base(repo) }
  let(:described_instance) { Git::Repository.new(execution_context: execution_context) }

  before do
    write_file('README.md', 'initial')
    repo.add('README.md')
    repo.commit('Initial commit')
    write_file('README.md', 'work in progress')
    repo.add('README.md')
  end

  describe '#stash_save_and_list' do
    it 'returns the new stash entry after saving' do
      result = described_instance.stash_save_and_list(message: 'wip')
      expect(result).to all(be_a(Git::Stash))
      expect(result.first.message).to include('wip')
    end
  end
end

One context 'when the command succeeds' block (or just it blocks directly under describe) per facade method, with one or more variations that exercise the orchestration sequence or post-processing. Do not add a context 'when the command fails' block — error wrapping is the command's concern and is covered by command integration tests.

What integration tests assert

  • The Ruby return value's structure and key fields (e.g., classes, required attributes, presence of expected entries).
  • Multi-command orchestration produces the documented end-to-end value, not intermediate command results.
  • For signature-compatibility behavior that is user-visible at runtime, integration coverage may assert that legacy call shapes are accepted.

Review checks for signature policy

When reviewing existing facade tests, add these checks:

  1. If the method is legacy-contract, unit tests cover required call shapes.
  2. Test expectations validate public contract behavior, not only command delegation internals.

What integration tests do not assert

  • Specific CLI tokens reaching git (covered by command unit tests).
  • Specific git output formatting (testing git, not the facade).
  • Edge cases that vary between git versions in immaterial ways. Anchor assertions on stable inputs (paths, ref names) the test controls — not on git message phrasing.

Workflow

  1. Load the RSpec Unit Testing Standards skill (line 1 through EOF).
  2. Read the spec file(s) under review and the corresponding facade module (lib/git/repository/<topic>.rb) plus the underlying Git::Commands::* and Git::Parsers::* files the facade calls.
  3. Audit each spec against the rules in Reference, checking unit and integration tests separately.
  4. Produce the Output.

Output

When writing new facade tests

Produce the unit and (when applicable) integration spec files following the patterns above. Then self-verify by running every checklist item in the Reference section against your output.

When reviewing existing facade tests

Provide:

  1. issue table

    Check Status Issue
  2. corrected snippets for failing checks

  3. Self-verify before concluding — re-run the reference against your proposed snippets until all checks pass.

Branch workflow: Implement any new or updated tests on a feature branch. Never commit or push directly to main — open a pull request when changes are ready to merge.

Install via CLI
npx skills add https://github.com/ruby-git/ruby-git --skill facade-test-conventions
Repository Details
star Stars 1,796
call_split Forks 531
navigation Branch main
article Path SKILL.md
More from Creator