| name | increase-test-coverage |
| description | Increase Go test coverage to 90%+ using a Research → Plan → Implement pipeline. Analyzes coverage gaps, generates table-driven tests with httptest mocks, and validates results with go test -coverprofile. Designed for Go MCP server projects using the official go-sdk and gitlab.com/gitlab-org/api/client-go/v2. |
Increase Test Coverage
Primary Directive
Systematically increase Go test coverage to 90%+ per package using a structured Research → Plan → Implement pipeline. Generate comprehensive, buildable, passing tests that follow project conventions and use proper mocks for external dependencies.
All tests must:
- Compile and pass on the first run
- Use existing test helpers and patterns found in the codebase
- Mock external dependencies (GitLab API) via
httptest
- Follow table-driven test patterns with
t.Run() subtests
- Cover happy paths, edge cases, and error scenarios
- Include documentation (doc comment) explaining what each test validates and why
- Be verified against false passes (assertions that never fail)
- Be written in English per project language policy
Execution Context
This skill is designed for the Test Expert agent or any agent tasked with increasing test coverage. It operates on Go codebases that use the standard testing package, net/http/httptest, and optionally testify/assert.
Pipeline Overview
flowchart TD
start[Increase test coverage]
research[Research\nMeasure coverage and analyze gaps]
plan[Plan\nPrioritize gaps by package]
implement[Implement\nWrite tests per phase]
build[Build\ngolangci-lint and go build]
test[Test\ngo test]
fix[Fix\nRepair errors]
start --> research --> plan --> implement
implement --> build
implement --> test
implement --> fix
fix --> build
fix --> test
Phase 1: Research
Thoroughly analyze the codebase and measure current coverage before writing any tests.
Step 1: Measure Baseline Coverage
Run coverage analysis for the entire project:
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
Record the per-package and per-function coverage percentages. This is the baseline.
Step 2: Generate HTML Coverage Report (Optional)
go tool cover -html=coverage.out -o coverage.html
Review the HTML report to visually identify uncovered code blocks.
Step 3: Identify Coverage Gaps
For each package, analyze:
- Uncovered functions — Functions with 0% coverage (highest priority)
- Partially covered functions — Functions below 90% (error paths, edge cases)
- Untested branches — Conditional logic where only one branch is covered
- Error handling paths —
if err != nil blocks that are never tested
- Edge cases — Boundary conditions, nil inputs, empty slices, zero values
Step 4: Discover Existing Test Patterns
Before writing any test, analyze the codebase for conventions:
| Convention | Where to Look |
|---|
| Test helpers | **/helpers_test.go — newTestClient(), respondJSON(), respondJSONWithPagination() |
| Mock patterns | How httptest.NewServer + http.HandlerFunc are used to mock GitLab API |
| Assertion style | Whether testify/assert or stdlib t.Errorf/t.Fatalf is used |
| Test naming | Pattern: TestFunctionName_Scenario or TestFunctionName_Scenario_ExpectedResult |
| Table-driven tests | Look for tests := []struct{} or cases := map[string]struct{} patterns |
| File placement | Tests in same package (white-box) or _test package (black-box) |
| Pagination helpers | paginationHeaders struct and respondJSONWithPagination() |
Step 5: Document Research Findings
Create a coverage analysis summary containing:
- Current overall coverage percentage
- Per-package coverage breakdown
- List of uncovered/partially covered functions ranked by priority
- Existing test patterns and helpers available
- Dependencies that need mocking (GitLab API endpoints)
Phase 2: Plan
Create a phased implementation plan that prioritizes maximum coverage impact.
Step 1: Prioritize Packages
Rank packages by coverage gap impact:
| Priority | Criteria |
|---|
| P0 — Critical | Core business logic with 0% coverage |
| P1 — High | Tool handlers, resource handlers below 80% |
| P2 — Medium | Helper functions, config loading below 90% |
| P3 — Low | Already above 90%, minor edge cases |
Step 2: Group into Phases
Divide work into 2-5 phases, each targeting a specific package or functional area:
Phase 1: [package] — Current: X% → Target: 90%+
- TestFunctionA_HappyPath
- TestFunctionA_ErrorCase
- TestFunctionA_EdgeCase_EmptyInput
...
Phase 2: [package] — Current: X% → Target: 90%+
...
Step 3: Define Test Cases per Function
For each uncovered function, specify:
- Happy path — Valid inputs producing expected outputs
- Error cases — Invalid inputs, API failures (4xx, 5xx), network errors
- Edge cases — Empty strings, zero values, nil pointers, large inputs
- Context cancellation — Cancelled or timed-out contexts
- Pagination — Multiple pages, empty results, single page
Step 4: Identify Required Mocks
For each test, document:
- Which GitLab API endpoint needs mocking (method + path)
- Expected request body validation (if any)
- Response status code and JSON payload
- Pagination headers (if applicable)
Step 5: Plan Structure
Present the plan to the user for confirmation before implementation. The plan should include:
- Phase breakdown with package targets
- Estimated number of new test functions per phase
- Expected coverage increase per phase
- New test helpers needed (if any)
- Total estimated coverage after all phases
Phase 3: Implement
Execute the plan phase by phase, validating after each phase.
Step 1: Implement One Phase at a Time
For each phase:
- Read source code — Understand the function signatures, logic branches, and dependencies
- Write test file — Create or extend
*_test.go files following existing patterns
- Use existing helpers — Reuse
newTestClient(), respondJSON(), respondJSONWithPagination()
- Create new helpers if needed — Add to
helpers_test.go for reusable mock patterns
Step 2: Test Writing Patterns
Table-Driven Tests (Preferred)
func TestFunctionName_Scenarios(t *testing.T) {
tests := []struct {
name string
input InputType
mockStatus int
mockBody string
wantErr bool
want OutputType
}{
{
name: "happy path",
input: InputType{Field: "value"},
mockStatus: http.StatusOK,
mockBody: `{"id": 1, "name": "test"}`,
want: OutputType{ID: 1, Name: "test"},
},
{
name: "not found",
input: InputType{Field: "missing"},
mockStatus: http.StatusNotFound,
mockBody: `{"message": "404 Not found"}`,
wantErr: true,
},
{
name: "empty input",
input: InputType{},
mockStatus: http.StatusBadRequest,
mockBody: `{"error": "field required"}`,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
respondJSON(w, tt.mockStatus, tt.mockBody)
}))
got, err := functionUnderTest(context.Background(), client, tt.input)
if (err != nil) != tt.wantErr {
t.Fatalf("error = %v, wantErr %v", err, tt.wantErr)
}
if !tt.wantErr && got != tt.want {
t.Errorf("got %+v, want %+v", got, tt.want)
}
})
}
}
Request Validation Mocks
When testing POST/PUT operations, validate the request:
client := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("method = %s, want POST", r.Method)
}
if r.URL.Path != "/api/v4/projects/42/issues" {
t.Errorf("path = %s, want /api/v4/projects/42/issues", r.URL.Path)
}
respondJSON(w, http.StatusCreated, `{...}`)
}))
Pagination Tests
client := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
respondJSONWithPagination(w, http.StatusOK, `[...]`, paginationHeaders{
Page: "1",
PerPage: "20",
Total: "50",
TotalPages: "3",
NextPage: "2",
})
}))
Context Cancellation Tests
func TestFunction_ContextCancelled(t *testing.T) {
client := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
respondJSON(w, http.StatusOK, `{}`)
}))
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err := functionUnderTest(ctx, client, input)
if err == nil {
t.Fatal("expected error for cancelled context, got nil")
}
}
Step 3: Validate After Each Phase
After writing tests for a phase:
- Compile:
go build ./...
- Static analysis:
make golangci-lint
- Run tests:
go test -v ./internal/[package]/...
- Measure coverage:
go test -coverprofile=coverage.out ./internal/[package]/...
- Check coverage:
go tool cover -func=coverage.out | grep [package]
If tests fail:
- Fix compilation errors immediately
- Adjust mock responses to match actual API behavior
- Verify test expectations match function implementation
Step 4: Track Progress
After each phase, update progress:
Package Before After Target Status
internal/tools 72% 91% 90% ✅ Done
internal/gitlab 65% — 90% 🔄 In Progress
internal/config 80% — 90% ⏳ Pending
internal/resources 70% — 90% ⏳ Pending
internal/prompts 68% — 90% ⏳ Pending
Step 5: Final Validation
After all phases complete:
- Run full test suite:
go test -race -coverprofile=coverage.out ./...
- Generate final coverage report:
go tool cover -func=coverage.out
- Verify every package meets 90%+ target
- Run quality checks:
make golangci-lint
- Refresh
docs/development/testing/testing.md: go run ./cmd/gen_testing_docs/
- Verify the generated testing reference:
go run ./cmd/gen_testing_docs/ --check
- Lint the generated testing reference:
npx markdownlint-cli2 docs/development/testing/testing.md
- Clean up any temporary files
Test Quality Standards
What to Test
| Category | Examples |
|---|
| Happy path | Valid inputs → expected output |
| Error responses | 400, 401, 403, 404, 422, 500 from GitLab API |
| Empty results | Empty arrays, null fields, missing optional fields |
| Input validation | Required fields missing, invalid IDs, empty strings |
| Pagination | First page, last page, single page, many pages |
| Context | Cancelled context, timed-out context |
| Edge cases | Very long strings, special characters, unicode, zero values |
What NOT to Test
- Third-party library internals (GitLab client SDK, MCP SDK)
- Trivial getters/setters with no logic
- Generated code (JSON schema tags, struct definitions)
- The Go standard library itself
Test Quality Checklist
Coverage Targets
| Package | Minimum Target | Stretch Goal |
|---|
internal/tools | 90% | 95% |
internal/resources | 90% | 95% |
internal/prompts | 90% | 95% |
internal/gitlab | 90% | 95% |
internal/config | 90% | 95% |
cmd/server | 80% | 90% |
| Overall | 90% | 95% |
Troubleshooting
Tests don't compile
- Check import paths match the module path in
go.mod
- Verify test file is in the correct package (same as source file)
- Ensure mock response JSON matches expected struct field names
- Run
make golangci-lint for quick diagnostics
Tests fail unexpectedly
- Print actual vs expected values with
%+v formatting
- Check mock handler serves the correct URL path and method
- Verify GitLab API response format (field names are snake_case in JSON)
- Check if function under test modifies input struct
Coverage doesn't increase
- Ensure test exercises the uncovered code path (check HTML report)
- Look for unreachable code or dead branches
- Some error paths require specific mock setups (e.g., network errors)
- Test both
if and else branches of conditionals
Race conditions detected
- Use
go test -race ./... to detect data races
- Ensure tests don't share mutable state
- Use
t.Parallel() carefully — only when tests are truly independent
- Check that test helpers don't store state across calls