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 produce real bytes — TarGzAsset a genuine gzipped tarball,
ChecksumsAsset a matching manifest, SignatureAsset a real OpenPGP signature —
so verification logic is tested against material that actually verifies.
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.
Mocks exist for all seven exported contract interfaces, including
ChecksumProvider and SignatureProvider — useful for driving the opt-in
branches without building a provider that implements them.
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.