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
- RSpec Unit Testing Standards — baseline RSpec rules that govern all unit test structure, naming, setup, stubbing, and coverage; this skill adds facade-specific conventions on top
- Facade Implementation — facade module structure and orchestration patterns
- Facade YARD Documentation — YARD docs for facade modules and methods
- Command Test Conventions — sibling skill
that tests the underlying
Git::Commands::*classes the facade calls
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:
- The facade constructs each command class with the injected
@execution_context. - Each command's
#callis invoked with the expected positional and keyword arguments (verifying argument pre-processing). - For multi-command sequences, the calls happen in the documented order.
- The parser/result-class is invoked with the command's stdout (when applicable).
- 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_contextis aninstance_double(Git::ExecutionContext::Repository)— never adouble('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(...).orderedto 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
CommandLineResultreturn — when the facade's contract is to return the command'sGit::CommandLineResultdirectly (not.stdoutand not a parser output), asserteq(<command>_result)to verify pass-through. - Option whitelisting — when the facade defines a
<METHOD>_ALLOWED_OPTSconstant and callsGit::Repository::Internal.assert_valid_opts!, test that an unknown key raisesArgumentErrorand 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 /
**optswhere 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#calland 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::Repositoryfor real. Useinstance_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.
#initializeof the facade module. The module is mixed intoGit::Repository; constructor coverage belongs torepository_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 (noslice— the assertion is the only enforcement mechanism)context 'deprecation handling'—Git::Deprecation.warnassertions and key-rewrite testscontext 'input validation'—ArgumentErrorraised 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 /**optsforms)
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:
- If the method is
legacy-contract, unit tests cover required call shapes. - 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
- Load the RSpec Unit Testing Standards skill (line 1 through EOF).
- Read the spec file(s) under review and the corresponding facade module
(
lib/git/repository/<topic>.rb) plus the underlyingGit::Commands::*andGit::Parsers::*files the facade calls. - Audit each spec against the rules in Reference, checking unit and integration tests separately.
- 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:
issue table
Check Status Issue corrected snippets for failing checks
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.