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>
This commit is contained in:
90
docs/usage/step-6-integration-tests.md
Normal file
90
docs/usage/step-6-integration-tests.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# 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`.
|
||||
Reference in New Issue
Block a user