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_testpackage 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+MustParseV1Xxxfunctions. 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 howcmp.Diffoutput is presented:(-want, +got): <diff>. All test files import this for consistent error messages.test/names(package names) —TestingSeed()callsutilrand.Seed(12345)to make Kubernetes name generators deterministic, preventing flaky tests caused by random resource names.pkg/reconciler/testing— Controller-level test infrastructure:factory.gobuilds reconciler assets;configmap.gocreates test ConfigMaps;logger.gocreates test loggers;status.goprovides helpers to assert onConditionstatuses.pkg/apis/config/testing—featureflags.go/defaults.goprovide 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 owntest/subdirectory inside the package.internal/test/annotation— Go AST-based scanner that reads e2e test files and enforces that everyTestXxxfunction is annotated as// parallelor// serial, and that serial tests supply a reason. Used inTestMainbefore the test run begins.- Generated fake clients (
pkg/client/*/fake) — Code-generated fromk8s.io/client-go/testingviacontroller-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 smalltest/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.gofiles - 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:35—TestGetSchedulabledefines a slice of 10 cases withname,finished []string, andexpectedTasks sets.String; each case runs int.Run(tc.name, ...)withcmp.Diffassertion. The DAG test is a textbook example: pure inputs, pure outputs, no Kubernetes machinery. - Scale:
pkg/reconciler/pipelinerun/pipelinerun_test.gois 19,215 lines;pkg/reconciler/taskrun/taskrun_test.gois 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.gofiles 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 inpkg/client/clientset/versioned/fakeprovide 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.Diffpervasively (1,169 occurrences in*_test.gofiles). There is no testifyassertorrequire. 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/cmpoptsis used heavily forcmpopts.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.gofile 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(orconformanceorexamples). Thetest/init_test.gofile carries//go:build conformance || e2e || examplesso that theTestMainsetup only compiles when one of these tags is active. Runninggo 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 (separatetest/conformance_test.go).examples— smoke tests for shipped YAML examples.
- Parallelism enforcement: The
internal/test/annotationAST scanner enforces that every e2eTestXxxfunction is annotated with// parallelor// serialin a doc comment. Serial tests must provide a reason. This is checked inTestMainbefore tests run — missing annotations are a hard error. The scanner uses Go’sgo/astandgo/parserpackages, making it a compile-time-style check at test startup. - Knative test infrastructure: E2e tests import
knative.dev/pkg/testfor kubeconfig loading, cluster targeting, and polling utilities.
Test quality observations#
What’s done well#
- Zero third-party assertion library. Using
cmp.Diff+ stdlibtestingkeeps the test dependency surface minimal and test output highly readable.cmpoptsprovides the flexibility that would otherwise require testify matchers. - YAML-inline fixtures.
test/parse/yaml.go’sMustParseV1Xxxfunctions 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/entrypointpackage’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/annotationto statically requiret.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.goat 19,215 lines andtaskrun_test.goat 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 usepackage 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.ymlCI 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 inentrypointer_test.go. This could be consolidated with a configurable behavior struct.
Patterns worth emulating#
test/parseYAML helper pattern: ProvidingMustParseXxx(t, yaml string) *TypedObjectfunctions is a clean, low-friction way to write fixture data for schema-heavy APIs. TheMustprefix convention (fatal on error, no error return) keeps test code linear.diff.PrintWantGotconvention: A one-function package that standardises diff output format is worth adopting in any project usingcmp.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 (intest/), while remaining completely invisible togo test ./...by default. - AST-driven test annotation enforcement: Using
go/astto enforce e2e test categorisation atTestMaintime is a novel and highly effective technique. It catches missingt.Parallel()calls before the test run starts, without requiring a linter plugin or separate tooling step.