Skip to content

Development & testing

For setup, commit conventions and the PR process, see CONTRIBUTING.md. This page explains how portcheck is tested and why.

Commands

Command What it does
make test unit tests with -race and shuffled order
make test-integration adds -tags integration: real sockets, processes and DNS
make cover-check integration tests + coverage gate (.testcoverage.yml)
make check lint + actionlint + govulncheck + cover-check (CI additionally checks that docs/commands is up to date and builds a full release snapshot)
make fuzz FUZZTIME=1m every fuzz target for the given time
make mutation mutation testing of pkg/ with gremlins
make bench benchmarks
make docs regenerate the CLI reference and build this site with --strict
make demo re-record docs/assets/demo.gif with vhs

Test layers

Layer Technique Where
Pure logic table-driven tests, t.Parallel(), go-cmp diffs everywhere
Parsers native Go fuzzing (FuzzParse, FuzzExpand) pkg/portspec, pkg/scan
OS boundaries hand-written fakes for Provider, ops, Dialer, Resolver, bind pkg/*
Real OS behavior //go:build integration: real listeners, child processes killed for real, loopback scans pkg/*/integration_test.go
Commands in-process tests with a fake App; every flag, error path and exit code internal/cli/*_test.go
Real binary testscript scenarios against the compiled CLI and a helper process holding a port internal/cli/testdata/script/*.txtar
Rendering golden files for every format and TUI screen internal/*/testdata/*.golden
Test quality mutation testing (weekly in CI) .github/workflows/mutation.yml

Coverage policy

CI runs the full suite, integration tests included, on Linux, macOS and Windows, merges the three coverage profiles, and enforces:

  • 100% line coverage for pkg/* (the public library);
  • ≥ 95% per package and in total, and ≥ 90% per file, elsewhere.

OS-specific files (*_windows.go, *_unix.go) are only measured on their own OS, which is why profiles are merged before the gate. The only exclusions are cmd/portcheck/main.go (one line calling cli.Main, which is tested directly), test-only helpers (internal/testutil) and the documentation generator (internal/cli/gendocs, exercised by CI's docs drift check); they are listed explicitly in .testcoverage.yml.

Coverage shows that every line ran, not that its result was checked. Mutation testing (make mutation) covers that gap: it changes the code and verifies that a test fails.

Golden files

After an intentional change to output or the TUI:

go test ./internal/output/... ./internal/tui/... -update
git diff internal/*/testdata   # review every change

Fakes, not mocks

Tests use small hand-written fakes (inspect.FakeProvider, fakeDialer, fakeOps, …) that model behavior: a fake process that ignores SIGTERM, a dialer that times out, and so on. They don't record expected call sequences, so tests stay readable and don't break on refactors.