Tekton Pipelines — Testing#

Test metrics#

  • Test files: 278
  • Total Go files: 983
  • Ratio (test files / source files): ~0.28 (roughly 1 test file per 3.5 source files)
  • Test frameworks: stdlib testing + github.com/google/go-cmp/cmp — no testify, no gomock, no ginkgo

Test organization#

  • Placement: Mixed — same package (white-box) for reconcilers and most packages (e.g., package taskrun), external _test package for pure domain packages (e.g., package dag_test). No consistent rule, but the dominant pattern is same-package white-box tests.
  • Helper packages:
    • test/parse (package parse) — A single file (yaml.go) with 20+ MustParseV1Xxx functions. Each function accepts a YAML string and deserializes it into a typed Kubernetes object using the generated scheme. Fatal on parse error. This is the backbone of how all fixture objects are created in tests — inline YAML rather than Go struct literals.
    • test/diff (package diff) — One function, PrintWantGot(diff string) string, that standardises how cmp.Diff output is presented: (-want, +got): <diff>. All test files import this for consistent error messages.
    • test/names (package names) — TestingSeed() calls utilrand.Seed(12345) to make Kubernetes name generators deterministic, preventing flaky tests caused by random resource names.
    • pkg/reconciler/testing — Controller-level test infrastructure: factory.go builds reconciler assets; configmap.go creates test ConfigMaps; logger.go creates test loggers; status.go provides helpers to assert on Condition statuses.
    • pkg/apis/config/testingfeatureflags.go / defaults.go provide test-ready config structs with sensible defaults injected into context.
    • pkg/spire/test — Dedicated SPIRE PKI test infrastructure: fake SPIFFE workload API, fake bundle endpoint, X.509 certificate utilities, PEM helpers. Isolated in its own test/ subdirectory inside the package.
    • internal/test/annotation — Go AST-based scanner that reads e2e test files and enforces that every TestXxx function is annotated as // parallel or // serial, and that serial tests supply a reason. Used in TestMain before the test run begins.
    • Generated fake clients (pkg/client/*/fake) — Code-generated from k8s.io/client-go/testing via controller-gen. Provide in-memory fake Kubernetes API servers for controller unit tests.
  • Fixtures: Inline YAML strings (via test/parse) are the primary fixture mechanism — no external fixture files in the main test path. A small test/testdata/ directory exists for select e2e YAML examples. pkg/pod/testdata/ holds golden files for pod spec comparison.

Test patterns#

Table-driven tests#

  • Prevalence: Heavy — 1,651 occurrences of t.Run, table range patterns, or named test cases in *_test.go files
  • Style: Anonymous struct slice (for _, tc := range []struct{...}{{...}}{...}) is most common. Named struct types (type testCase struct) appear in longer test functions. Map-based tables are not used.
  • Example: pkg/reconciler/pipeline/dag/dag_test.go:35TestGetSchedulable defines a slice of 10 cases with name, finished []string, and expectedTasks sets.String; each case runs in t.Run(tc.name, ...) with cmp.Diff assertion. The DAG test is a textbook example: pure inputs, pure outputs, no Kubernetes machinery.
  • Scale: pkg/reconciler/pipelinerun/pipelinerun_test.go is 19,215 lines; pkg/reconciler/taskrun/taskrun_test.go is 8,226 lines. These are among the largest test files encountered across the 50-project survey. Each represents a comprehensive table of reconciler scenarios.

Mocking approach#

  • Strategy: Manual fakes defined in _test.go files for domain interfaces; code-generated fake Kubernetes clients for infrastructure.
  • Domain fakes: Small, purpose-built structs that implement 1-3 method interfaces. Examples in pkg/entrypoint/entrypointer_test.go:2157: fakeWaiter, fakeRunner, fakePostWriter, fakeErrorWaiter, fakeErrorRunner, fakeTimeoutRunner, fakeExitErrorRunner, fakeLongRunner, fakeResultsWriter — each implements the corresponding interface (Waiter, Runner, PostWriter, ResultsWriter). These fakes are file-local (type fakeXxx struct) and are not exported.
  • Infrastructure fakes: fakekubeclientset "k8s.io/client-go/kubernetes/fake" and the project’s own generated fakes in pkg/client/clientset/versioned/fake provide in-memory Kubernetes API servers. Controller tests wire these up in place of a real API server. This is the standard Kubernetes controller testing approach — no testcontainers, no kind cluster needed for unit tests.
  • No gomock: All mocking is manual. This keeps test code readable (no mock expectations to set up) but requires writing boilerplate fake structs. For small interfaces (1-3 methods), this is zero friction.

Comparison approach#

  • go-cmp as the assertion library: The project uses github.com/google/go-cmp/cmp.Diff pervasively (1,169 occurrences in *_test.go files). There is no testify assert or require. Assertions follow the pattern:
    if d := cmp.Diff(want, got, opts...); d != "" {
        t.Errorf("unexpected diff: %s", diff.PrintWantGot(d))
    }
  • cmpopts: github.com/google/go-cmp/cmp/cmpopts is used heavily for cmpopts.IgnoreFields, cmpopts.SortSlices, cmpopts.EquateEmpty, enabling precise structural comparison of complex Kubernetes objects while ignoring irrelevant metadata.

Integration tests#

  • Present: Yes — a dedicated test/ package at the repository root
  • How: Tests run against a real Kubernetes cluster. The test/clients.go file creates typed clients (Kubernetes + Tekton CRD clients via generated clientsets + Knative test utilities). Tests create real TaskRuns and PipelineRuns and poll until they reach terminal state.
  • Separation: Build tags are the separation mechanism — every e2e test file starts with //go:build e2e (or conformance or examples). The test/init_test.go file carries //go:build conformance || e2e || examples so that the TestMain setup only compiles when one of these tags is active. Running go test ./... without tags produces only unit tests.
  • Categories: Three distinct e2e tag categories:
    • e2e — functional end-to-end tests (TaskRun, PipelineRun, resolvers, workspaces, retries, timeouts, cancellation, etc.). ~50 test files.
    • conformance — Tekton API conformance tests (separate test/conformance_test.go).
    • examples — smoke tests for shipped YAML examples.
  • Parallelism enforcement: The internal/test/annotation AST scanner enforces that every e2e TestXxx function is annotated with // parallel or // serial in a doc comment. Serial tests must provide a reason. This is checked in TestMain before tests run — missing annotations are a hard error. The scanner uses Go’s go/ast and go/parser packages, making it a compile-time-style check at test startup.
  • Knative test infrastructure: E2e tests import knative.dev/pkg/test for kubeconfig loading, cluster targeting, and polling utilities.

Test quality observations#

What’s done well#

  • Zero third-party assertion library. Using cmp.Diff + stdlib testing keeps the test dependency surface minimal and test output highly readable. cmpopts provides the flexibility that would otherwise require testify matchers.
  • YAML-inline fixtures. test/parse/yaml.go’s MustParseV1Xxx functions allow test data to be written in the natural form (YAML) of the objects being tested, without the verbosity of Go struct literals. This is especially valuable for Kubernetes resources with deeply nested specs.
  • Deterministic name generation. names.TestingSeed() ensures that any test that relies on generated names is repeatable. This eliminates a whole class of test flakiness.
  • Domain isolation. The DAG package (pkg/reconciler/pipeline/dag) has zero Kubernetes imports and is tested with pure table-driven unit tests. This is the right level of abstraction — the scheduler algorithm is validated independently of any controller machinery.
  • Small manual fakes over gomock. The pkg/entrypoint package’s 9 fake types are each 5-15 lines. They are easier to read, debug, and maintain than generated mocks. This scales well for the small-interface style the project uses.
  • Annotation-enforced e2e parallelism. Using internal/test/annotation to statically require t.Parallel() declarations prevents the common operator testing mistake of running all e2e tests serially when they could safely run in parallel.
  • Rich e2e coverage. The test/ directory contains ~50 e2e test files covering features exhaustively: cancellation, retries, timeouts, workspaces, sidecars, results, matrix expansion, custom tasks, resolvers, affinity, hermetic execution, windows nodes, trusted resources. The breadth of e2e coverage matches the complexity of the system.

What could improve#

  • Massive test files. pipelinerun_test.go at 19,215 lines and taskrun_test.go at 8,226 lines are unwieldy. Navigating them requires IDE support; understanding the full test matrix without context is difficult. Splitting by concern (e.g., pipelinerun_params_test.go, pipelinerun_matrix_test.go) would improve maintainability.
  • Mixed package placement. Some test files use package taskrun (white-box) while others use package dag_test (black-box) with no documented rationale. A project-wide convention would reduce cognitive load.
  • No test coverage enforcement in CI visible from source. The go-coverage.yml CI file exists but there is no visible coverage threshold gating PRs. Coverage discipline appears to rely on reviewer judgement rather than automation.
  • Fake duplication. Multiple similarly named fakes (fakeWaiter, fakeErrorWaiter, fakeTimeoutRunner, fakeExitErrorRunner) in the same test file with subtle behavioral differences — all defined after line 2157 in entrypointer_test.go. This could be consolidated with a configurable behavior struct.

Patterns worth emulating#

  • test/parse YAML helper pattern: Providing MustParseXxx(t, yaml string) *TypedObject functions is a clean, low-friction way to write fixture data for schema-heavy APIs. The Must prefix convention (fatal on error, no error return) keeps test code linear.
  • diff.PrintWantGot convention: A one-function package that standardises diff output format is worth adopting in any project using cmp.Diff. It eliminates the (-want, +got) / (-got, +want) inconsistency that plagues multi-author test suites.
  • Build-tag-based test categories: Using //go:build e2e (not a filename suffix or directory) to separate unit and e2e tests allows the e2e tests to live alongside their feature code (in test/), while remaining completely invisible to go test ./... by default.
  • AST-driven test annotation enforcement: Using go/ast to enforce e2e test categorisation at TestMain time is a novel and highly effective technique. It catches missing t.Parallel() calls before the test run starts, without requiring a linter plugin or separate tooling step.