Skip to content

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.