docs/developers/testing-best-practices.md
This document outlines best practices for writing unit tests in the Tekton Pipeline project.
t.Fatalf vs t.ErrorfIn Go tests, both t.Fatalf and t.Errorf are used to report test failures, but they behave differently:
t.Errorf → reports an error and continues executiont.Fatalf → reports an error and stops the test immediatelyChoosing the right one improves test clarity, reliability, and debugging experience.
t.Fatalf()Stop the test immediately when continuing is impossible or unsafe.
func TestTaskRunReconcile(t *testing.T) {
clients, err := test.NewClients(kubeconfig, cluster, namespace)
if err != nil {
t.Fatalf("failed to create test clients: %v", err)
}
// Test continues - clients are guaranteed to be valid
}
Why? Without clients, every subsequent operation will fail or panic.
t.Errorf()Continue testing to collect all failures in one run.
func TestTaskRunStatus(t *testing.T) {
tr := getTaskRun(t)
// Check multiple properties - want to see all failures
if tr.Status.PodName == "" {
t.Errorf("expected PodName to be set, got empty string")
}
if tr.Status.StartTime == nil {
t.Errorf("expected StartTime to be set, got nil")
}
if len(tr.Status.Steps) != 3 {
t.Errorf("expected 3 steps, got %d", len(tr.Status.Steps))
}
}
Why? Each check is independent. Seeing all failures helps fix them faster.
Helper functions are commonly used for test setup and must-succeed operations.
t.Fatalf() in HelpersIn most cases, helper functions should call t.Fatalf() directly instead of returning errors.
This is the idiomatic Go pattern and aligns with both the
Google Go Style Guide and Tekton’s existing conventions (e.g., MustParse* helpers).
t.Fatalf()func mustCreateTaskRun(t *testing.T, name string) *v1.TaskRun {
t.Helper()
tr := &v1.TaskRun{
ObjectMeta: metav1.ObjectMeta{Name: name},
}
created, err := clients.TektonClient.TaskRuns("default").Create(ctx, tr, metav1.CreateOptions{})
if err != nil {
t.Fatalf("failed to create TaskRun: %v", err)
}
return created
}
Why?
Do not call t.Fatalf() inside a goroutine:
go func() {
t.Fatalf("this will panic, not fail the test correctly")
}()
Why?
t.Fatalf() must be called from the test’s main goroutine.
Calling it inside another goroutine causes a panic, not a proper test failure.
Returning errors from helpers is appropriate only when:
func createTaskRun(name string) (*v1.TaskRun, error) {
tr := &v1.TaskRun{
ObjectMeta: metav1.ObjectMeta{Name: name},
}
created, err := clients.TektonClient.TaskRuns("default").Create(ctx, tr, metav1.CreateOptions{})
if err != nil {
return nil, fmt.Errorf("failed to create TaskRun: %w", err)
}
return created, nil
}
| Scenario | Use | Reason |
|---|---|---|
| Test setup fails | t.Fatalf() | Cannot proceed safely |
| Critical preconditions fail | t.Fatalf() | Avoid invalid test state |
| Multiple independent checks | t.Errorf() | Report all failures |
| Assertions | t.Errorf() | Continue test execution |
| Helper functions (default) | t.Fatalf() | Idiomatic and simpler |
| Helpers in goroutines | Return error | t.Fatalf() is unsafe |
Golden Rule: Use t.Fatalf() when continuing the test is impossible or meaningless. Use t.Errorf() when you want to collect and report multiple failures in a single run.