Testing

How to run tests, write new tests, and understand the test structure.

Quick Start

# Run all tests
cd backend && go test ./... -count=1

# Run with verbose output
go test ./... -v -count=1

# Run tests for a specific package
go test ./internal/vex/ -v -count=1

# Run a single test by name
go test ./internal/vex/ -run TestNormalizeVulnID -v

# Run with race detection (used in CI)
go test ./... -count=1 -race

# Check coverage
go test ./... -coverprofile=coverage.out
go tool cover -html=coverage.out

Test Structure

Tests live next to the code they test, using Go’s _test.go convention:

backend/
├── cmd/
│   └── api-gateway/
│       ├── main.go
│       └── main_test.go            ← auth middleware, input validation
├── internal/
│   ├── clickhouse/
│   │   ├── client.go
│   │   └── client_test.go          ← query method signatures, cluster helpers
│   ├── config/
│   │   ├── config.go
│   │   └── config_test.go          ← Load(), S3 buckets, auth, ignore prefix, shared settings
│   ├── cyclonedx/
│   │   ├── parser.go
│   │   └── parser_test.go          ← CycloneDX parsing
│   ├── github/
│   │   ├── purl.go
│   │   ├── purl_test.go
│   │   ├── resolver.go
│   │   └── resolver_test.go
│   ├── license/
│   │   ├── checker.go
│   │   └── checker_test.go
│   ├── osv/
│   │   ├── client.go
│   │   └── client_test.go
│   ├── osvutil/
│   │   ├── osvutil.go
│   │   └── osvutil_test.go
│   ├── protobomparser/
│   │   ├── parser.go
│   │   └── parser_test.go          ← protobom backend detection
│   ├── repo/
│   │   ├── scanner.go
│   │   └── scanner_test.go         ← file scanning, ignore prefix, generic JSON
│   ├── s3/
│   │   ├── client.go
│   │   └── client_test.go
│   ├── sbom/
│   │   ├── dispatch.go
│   │   └── dispatch_test.go        ← multi-format detection
│   ├── spdx/
│   │   ├── parser.go
│   │   └── parser_test.go
│   └── vex/
│       ├── parser.go
│       └── parser_test.go
├── pkg/
│   ├── dto/
│   │   └── dto_test.go             ← JSON serialization, fields
│   └── models/
│       └── models_test.go          ← cluster fields, omitempty

Current Test Inventory

PackageTestsSubtestsWhat’s Covered
cmd/api-gateway237Auth middleware (Bearer/API-Key/disabled), input validation, public paths
internal/clickhouse40Query method signatures, SanitizeClusterName
internal/config170Defaults, env vars, S3 buckets JSON, shared credentials, shared settings inheritance, auth modes, IgnorePrefix
internal/cyclonedx30CycloneDX parsing (minimal, full, rejection)
internal/github3522ExtractGitHubRepo (19 PURL patterns), RepoKey, Resolve, ResolveWithMetadata, cache, preload
internal/license4420Categorize, Check, policy, exceptions, prefix matching, Go temp names
internal/osv60QueryBatch, errors, cancellation, cache
internal/osvutil4035Severity, CVSS, ComputeCVSSv3BaseScore, fixed versions, affected versions
internal/protobomparser20Backend detection, opt-in dispatch
internal/repo70File scanning, ignore prefix, generic JSON, SHA256, nested dirs
internal/s32520ClassifyKey, ParseURI, BucketConfig defaults, ObjectInfo
internal/sbom40Multi-format detection (SPDX, CycloneDX, in-toto)
internal/spdx157Full parse, in-toto, invalid JSON, deterministic IDs, GoTemp, CleanPackageName
internal/vex138Parse, normalizeVulnID, URL patterns
pkg/dto30JSON serialization, ProjectListItem, ClusterStats
pkg/models60Cluster fields, omitempty, propagation
Total247119366 test invocations

Test Patterns

Table-Driven Tests

func TestCategorize(t *testing.T) {
    tests := []struct {
        input    string
        expected Category
    }{
        {"MIT", CategoryPermissive},
        {"GPL-3.0-only", CategoryCopyleft},
    }
    for _, tt := range tests {
        t.Run(tt.input, func(t *testing.T) {
            got := Categorize(tt.input)
            if got != tt.expected {
                t.Errorf("Categorize(%q) = %q, want %q", tt.input, got, tt.expected)
            }
        })
    }
}

httptest Mock Server

func TestQueryBatch_MockServer(t *testing.T) {
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"results": [{"vulns": [...]}]}`))
    }))
    defer server.Close()
    // Use server.URL as base URL...
}

t.TempDir() for Filesystem Tests

func TestScanner_Scan(t *testing.T) {
    tmpDir := t.TempDir()
    os.WriteFile(filepath.Join(tmpDir, "test.spdx.json"), []byte(`{...}`), 0644)
    scanner := NewScanner(tmpDir)
    files, err := scanner.Scan()
    // Assert...
}

Test Requirements

  • No external dependencies. No running ClickHouse, network, or Docker needed.
  • No test order dependency. Each test is self-contained.
  • Race-safe. All tests must pass with -race flag.
  • Use subtests (t.Run()) for table-driven tests.

CI Integration

Tests run automatically on every push/PR:

- name: Test
  working-directory: backend
  run: go test ./... -count=1 -race

Angular (Frontend) Tests

The Angular frontend uses Vitest (not Karma/Jasmine). Tests live alongside components as *.spec.ts files.

Quick Start

cd ui

# Run all tests
npx ng test

# Run once (no watch)
npx ng test --watch=false

Current Test Inventory (Frontend)

Spec FileTestsWhat’s Covered
app.spec.ts3App creation, navbar brand, navigation links
api.service.spec.ts16All HTTP methods, error handling, pagination params
dashboard.component.spec.ts2Component creation, data loading
sbom-list.component.spec.ts2Component creation, SBOM list loading
sbom-detail.component.spec.ts4Tab switching, vuln/license/dep views
vulnerability-list.component.spec.ts2Component creation, vuln list loading
license-overview.component.spec.ts2Component creation, license data
vex-list.component.spec.ts2Component creation, VEX statement loading
cve-impact.component.spec.ts2CVE search, project listing
dependency-stats.component.spec.ts2Top dependencies, unique deps counter
license-violations.component.spec.ts3Violations tab, exceptions tab
version-skew.spec.ts3Paginated loading, search
package-search.spec.ts8Search, expandable results, detail navigation
archived-packages.component.spec.ts3Data loading, grouped display
project-list.component.spec.ts3Project loading, search with debounce
Total5715 spec files

Test Patterns (Angular)

  • Model tests — Verify TypeScript interfaces match API shapes (no HTTP mocking needed)
  • Component tests — Use TestBed with provideHttpClientTesting() for HTTP mocking
  • OnPush strategy — Tests call fixture.detectChanges() and verify DOM output