Test against a forge¶
Three tools, for three different jobs. Picking the wrong one is the usual reason forge-related tests are slow, flaky, or prove nothing.
| You are testing | Use |
|---|---|
| Your consumer logic: update flows, version comparison, policy | the test package's Source |
A specific interaction: that you called Push, that you handled an error |
the published mocks |
| Your own provider implementation | the conformance harness |
A real provider, with no network¶
Source is a complete, in-memory Provider. Because it is a real
implementation rather than a mock, code under test exercises the real contract,
including the fallback paths that mocks usually paper over:
import forgetest "gitlab.com/phpboyscout/go/forge/test"
src := forgetest.New(
forgetest.WithRelease("v1.2.3", forgetest.TarGzAsset("mytool", "mytool", "#!/bin/sh\n")),
forgetest.WithLatestTag("v1.2.3"),
)
updater := myapp.NewUpdater(src)
Options cover the cases worth testing deliberately:
| Option | Sets up |
|---|---|
WithRelease(tag, assets...) |
A release and its assets |
WithLatestTag(tag) |
Which release is "latest" |
WithMissingTag(tag) |
A tag that must not resolve |
WithChecksumManifest(b) |
Opts into ChecksumProvider |
WithSignatureManifest(b) |
Opts into SignatureProvider |
WithDownloadError(err) |
A download that fails |
Asset builders¶
They produce real bytes (a genuine gzipped tarball, a matching manifest, a real OpenPGP signature) so verification logic is tested against material that actually verifies:
| Builder | Produces |
|---|---|
AssetName(tool) |
The conventional asset filename for this OS/arch |
TarGzAsset(tool, bin, body) |
A real gzipped tarball containing bin |
Manifest(corrupt, assets...) |
checksums.txt bytes |
ChecksumsAsset(corrupt, assets...) |
A checksums.txt asset |
SignatureAsset(entity, manifest, bad) |
A checksums.txt.sig asset |
Test that verification REJECTS a bad artefact¶
The corrupt and bad flags are the point of these builders, and the half most
worth testing. Verifying that a good artefact passes proves little, because
a verifier that accepts everything also passes. What matters is that a tampered
one is refused:
binary := forgetest.TarGzAsset("mytool", "mytool", "#!/bin/sh\n")
// checksums.txt that NAMES the binary but hashes different bytes
src := forgetest.New(
forgetest.WithRelease("v1.2.3",
binary,
forgetest.ChecksumsAsset(true, binary),
),
forgetest.WithLatestTag("v1.2.3"),
)
_, err := myapp.NewUpdater(src).Update(ctx)
require.Error(t, err, "a mismatched checksum must abort the update")
SignatureAsset(entity, manifest, true) does the same for signatures: it signs
different bytes, so the signature is well-formed and verifies against nothing.
That distinguishes "rejected because the signature is invalid" from "rejected
because the file was missing", which are different failures worth telling
apart.
None of the builders take a *testing.T, so the same fixtures work outside a
test binary. That is deliberate: it lets a tool build a stub release server for
manual or end-to-end use.
Opt-in capabilities are off by default
A bare Source returns ErrNotSupported from both optional methods, so
consumers exercise the asset-by-name fallback unless you opt in. That is
the path most releases take in production, so it is the right default to test
against.
Mocks for interaction tests¶
When the assertion is that you made a call, rather than what came back, use the published mocks:
import forgemocks "gitlab.com/phpboyscout/go/forge/mocks"
provider := forgemocks.NewMockProvider(t)
provider.EXPECT().
GetLatestRelease(mock.Anything, "acme", "tool").
Return(nil, forge.ErrReleaseNotFound).
Once()
Alias the import. A bare mocks collides the moment a test needs mocks from two
modules, and the same goes for test. The module path already says which
project these belong to, so the leaf stays a plain noun and the alias supplies
the reading context. NewMockProvider(t) registers cleanup, so unmet expectations fail the
test on their own.
Twelve mocks are published, covering the contract and the optional capabilities:
MockProvider |
MockRelease |
MockReleaseAsset |
MockChecksumProvider |
MockSignatureProvider |
MockConfig |
MockRepositories |
MockContents |
MockSites |
MockIssues |
MockIssueFiler |
MockSnippets |
The optional-capability mocks are the useful ones: they drive the opt-in branches without you building a provider that implements them.
Authenticator, KeyManager and Prompter are not mocked. They are small
enough to hand-write, and Prompter is a single method your CLI implements
anyway.
Prefer the real thing for behaviour¶
A mock proves you called the API. Only a real provider proves you called it
correctly. Reach for mocks when the forge interaction is incidental to what you
are testing, and for Source when it is the point.
Related¶
- Author a provider
- Conformance harness: every
ConformanceConfigfield, and what a blank one costs - Verify a release before you trust it: a
worked example built on
Source - Optional capabilities