Files
http-latency-prober/docs/usage/step-6-integration-tests.md
Jan Novak a9534ec2c1 test(go): step 6 — integration tests covering all exit codes and output
Extract run(args, stdout, stderr) int from main() for in-process
testability. Fix TLS failure classification (tlsErr now captured from
TLSHandshakeDone hook). Add run_test.go with 14 table-driven in-process
tests and cli_test.go with TestMain + 3 subprocess smoke tests. All
servers use httptest; .invalid TLD for deterministic DNS failures.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 01:00:20 +02:00

91 lines
3.6 KiB
Markdown

# Step 6 — Integration Tests
## What this step delivers
A full integration-test suite covering every success and failure mode of the
`latprobe` CLI. Tests are written using the Go standard library only (`testing`,
`net/http/httptest`, `os/exec`, `encoding/json`) — no third-party dependencies.
Two test layers:
| Layer | File | How it runs |
|-------|------|-------------|
| In-process | `go/run_test.go` | Calls `run(args, stdout, stderr)` directly; fast, covers the full matrix |
| Subprocess | `go/cli_test.go` | Builds the real binary once, `exec.Command`s it; exercises the true `os.Exit` path |
## Running the tests
```sh
cd go
# Run all tests (quiet)
go test ./...
# Run with per-case output
go test -v ./...
# Run only in-process tests
go test -v -run TestRunMatrix .
go test -v -run TestJSON .
# Run only subprocess smoke tests
go test -v -run TestCLI .
```
## Test matrix
### In-process (`run_test.go`)
| Test case | Exit code | Assertion |
|-----------|-----------|-----------|
| Success 200 | 0 | stdout contains `(200)`, `TCP connect`, `Total` |
| HTTP 500, no `--fail` | 0 | stdout contains `(500)`, `Total` |
| HTTP 404, `--fail` | 6 | stdout contains `404 ✗` |
| DNS failure (`.invalid` TLD) | 2 | stdout contains `✗ dns:` |
| Connection refused (listen-then-close) | 3 | stdout contains `✗ connect:` |
| Timeout (`--timeout 200ms`, blocking handler) | 4 | stdout contains `✗ timeout:` |
| TLS failure (self-signed cert) | 5 | stdout contains `✗ tls:` |
| Multiple URLs (200 + `.invalid`) | 2 (highest) | stdout contains both `(200)` and `✗ dns:` |
| Sampling `-n 3`, all success | 0 | stdout contains `3 samples`, `min`, `avg`, `max` |
| No args | 1 | stderr contains `Usage:` |
| `-h` | 0 | stderr contains `Usage:` |
| `--json` success | 0 | valid JSON, `phases.total` present, `failed == 0` |
| `--json` DNS failure | 2 | valid JSON, `errors[0].phase == "dns"`, `succeeded == 0` |
| `--json` `-n 3` success | 0 | `succeeded == 3`, `total.min_ms > 0`, `max_ms >= min_ms` |
### Subprocess smoke tests (`cli_test.go`)
`TestMain` builds the binary with `go build -o <tmp>/latprobe .` once before
any test runs. The binary is deleted on test completion.
| Test | Checks |
|------|--------|
| `TestCLISuccess` | Local server, exit 0, stdout has `(200)` and `Total` |
| `TestCLIDNSFailure` | `.invalid` host, real binary exits 2 |
| `TestCLIJSONDNSFailure` | `.invalid` host, JSON output, `errors[0].phase == "dns"` |
## Notes
- The `http: TLS handshake error` log line printed during the TLS test is the
**server-side** log of the client correctly rejecting the self-signed cert.
It is expected and harmless.
- `httptest.NewServer` binds to `127.0.0.1`; Go resolves loopback addresses
without a DNS query, so the DNS row does not appear in localhost test output.
Tests use `TCP connect` as the success-path phase assertion instead.
- The test suite requires no network access for any case except DNS failure,
which uses the reserved `.invalid` TLD (RFC 6761 — always NXDOMAIN).
## Code changes included in this step
Beyond the tests, two code changes were made:
1. **`main.go` refactor** — extracted `run(args []string, stdout, stderr io.Writer) int`
so the CLI is testable in-process. `main()` is now a one-liner:
`os.Exit(run(os.Args[1:], os.Stdout, os.Stderr))`. No behaviour change.
2. **`probe.go` TLS classification fix** — Go's `httptrace` calls
`TLSHandshakeDone` with the error on a failed handshake, so `tlsDone` was
set even on cert rejection. The previous classifier ("tlsStart set, tlsDone
zero") never matched. Fixed by capturing `tlsErr` from the hook and checking
it in `classifyErr`.