Files
temporal/docs/development/testing.md
Stephan Behnke 3ba31f2ac0 Add context-aware channel test helpers (#11700)
Adds generic `await.Rcv` and `await.Snd` helpers that bound blocking
channel operations by the test context and report closure or
cancellation clearly.

Replaces both previous helpers and existing unsafe channel rcv/snd
across tests.
2026-08-24 19:30:41 -07:00

314 lines
11 KiB
Markdown

# Testing
This document describes the project's testing setup, utilities and best practices.
## Setup
### Build tags
- `test_dep`: This Go build tag enables the test hooks implementation. Only very few tests require it; they will fail if not enabled.
- `TEMPORAL_DEBUG`: Extends functional test timeouts to allow sufficient time for debugging sessions.
- `disable_grpc_modules`: Disables gRPC modules for faster compilation during unit tests.
### Environment variables
- `CGO_ENABLED`: Set to `0` to disable CGO, which can significantly speed up compilation time.
- `TEMPORAL_TEST_LOG_FORMAT`: Controls the console output format for test logs. Available options: `json` or `console` (default)
- `TEMPORAL_TEST_LOG_LEVEL`: Sets the minimum verbosity level written to console output. Available levels: `debug` (default), `info`, `warn`, `error`, `fatal`
- `TEMPORAL_TEST_LOG_FILE`: Path to a file that receives a separate copy of test logs. When unset (default), file logging is disabled. Useful in CI to route debug-level logs to a downloadable artifact without flooding the job log.
- `TEMPORAL_TEST_LOG_FILE_FORMAT`: Output format for the log file. Available options: `json` (default) or `console`
- `TEMPORAL_TEST_LOG_FILE_LEVEL`: Minimum verbosity level written to the log file. Available levels: `debug` (default), `info`, `warn`, `error`, `fatal`
- `TEMPORAL_TEST_OTEL_OUTPUT`: Enables OpenTelemetry (OTEL) trace output for failed tests to the provided file path.
- `TEMPORAL_TEST_SHARED_CLUSTERS`: Number of shared clusters in the pool. Each can be used by multiple tests simultaneously.
- `TEMPORAL_TEST_DEDICATED_CLUSTERS`: Number of dedicated clusters in the pool. Each can be used by one test only at a time.
- `TEMPORAL_TEST_TIMEOUT`: Sets the duration timeout per test (e.g., `90s` for 90 seconds). This can be overridden per-test using `testcore.WithTimeout()`. The timeout is multiplied by `debug.TimeoutMultiplier` when debugging.
- `TEMPORAL_TEST_DATA_ENCODING`: Controls the encoding used for persistence DataBlobs. Available options: `proto3` (default) or `json`.
### Debugging via IDE
#### GoLand
For general instructions, see [GoLand Debugging](https://www.jetbrains.com/help/go/debugging-code.html).
To pass in the required build tags, add them to the "Go tool arguments" field in the Run/Debug configuration:
```
-tags disable_grpc_modules,test_dep
```
## Best Practices
### Use `require` instead of `assert`
Always use `require.X` (and `protorequire.X`) instead of `assert.X` (and `protoassert.X`).
`assert` records a failure but lets the test continue, which often leads to confusing
cascading errors.
### Polling with await.Require
For polling/retry loops in tests, use `await.Require` (or `await.Requiref`)
from `common/testing/await` instead of testify's `EventuallyWithT`.
Use `t.Context()` inside the callback for a context derived from the parent
context and canceled when the parent context is canceled or the await timeout
expires.
```go
await.Require(ctx, t, func(t *await.T) {
resp, err := client.GetStatus(t.Context())
require.NoError(t, err)
require.Equal(t, "ready", resp.Status)
}, 5*time.Second, 200*time.Millisecond)
```
Use `RequireTrue` instead of testify's `Eventually` for simple local bool-returning predicates.
```go
await.RequireTrue(t, func() bool {
return cache.Ready()
}, 5*time.Second, 200*time.Millisecond)
```
`RequireTrue` is the wrong tool when dealing with errors or assertions; use `Require` instead.
### Blocking on channels
Use `await.Rcv` and `await.Snd` for blocking channel operations.
They fail the test if its context ends or the channel closes.
```go
headers := await.Rcv(t, requestHeaders)
await.Snd(t, responses, response)
```
Keep an explicit `select` when testing non-blocking behavior or waiting on multiple channels.
### Parallelization
All tests (and subtests!) should use `t.Parallel()` to be run concurrently;
unless there is a reason not to.
`make parallelize-tests` can be used to automatically add `t.Parallel()`.
Use `//parallelize:ignore` to opt your test out of it.
## Test helpers
Test helpers can be found in the [common/testing](../../common/testing) package.
### parallelsuite package
Use `parallelsuite.Suite` to ensure your test suite is fast and safe: it runs all test methods and sub-tests in parallel by default;
and provides assertion helpers and safety mechanisms.
It replaces all use of `testify`'s `Suite`.
#### Context shorthand
```go
ctx := s.Context()
```
`s.Context()` returns the subtest-scoped context - equivalent to `testcontext.New(s.T())`.
#### Await shorthand
```go
s.Await(func(s *MySuite) {
resp, err := client.GetStatus(s.Context())
s.NoError(err)
s.Equal("ready", resp.Status)
}, 5*time.Second, 200*time.Millisecond)
```
Inside an `s.Await` callback, `s.Context()` is capped to that await's timeout.
### testvars package
Instead of creating identifiers like task queue name, namespace or worker identity by hand,
use the `testvars` package.
Example:
```go
func TestFoo(t *testing.T) {
tv := testvars.New(t)
req := &workflowservice.SignalWithStartWorkflowExecutionRequest{
RequestId: tv.Any().String(),
Namespace: tv.NamespaceName().String(),
WorkflowId: tv.WorkflowID(),
WorkflowType: tv.WorkflowType(),
TaskQueue: tv.TaskQueue(),
SignalName: tv.SignalName(),
}
}
```
Later you can assert on the generated values. `testvars` guarantees to provide the same value every time you call the same method.
```go
require.Equal(t, tv.WorkflowID(), startedWorkflow.WorkflowId)
```
If you need more than one value for the same entity in one test, you can use `WithEntityNumber()` method to
get a new instance of `testvars` with a different value.
```go
func TestFoo(t *testing.T) {
tv := testvars.New(t)
tv1 := tv.WithUpdateIDNumber(1)
tv2 := tv.WithUpdateIDNumber(2)
req1 := &workflowservice.UpdateWorkflowExecutionRequest{
Namespace: tv1.NamespaceName().String(),
WorkflowExecution: tv1.WorkflowExecution(),
Request: &updatepb.Request{
Meta: &updatepb.Meta{UpdateId: tv1.UpdateID()},
Input: &updatepb.Input{
Name: tv1.HandlerName(),
Args: payloads.EncodeString("args-value-of-" + tv1.UpdateID()),
},
},
}
req2 := &workflowservice.UpdateWorkflowExecutionRequest{
Namespace: tv2.NamespaceName().String(),
WorkflowExecution: tv2.WorkflowExecution(),
Request: &updatepb.Request{
Meta: &updatepb.Meta{UpdateId: tv2.UpdateID()},
Input: &updatepb.Input{
Name: tv2.HandlerName(),
Args: payloads.EncodeString("args-value-of-" + tv2.UpdateID()),
},
},
}
}
```
If you don't care about specific value, you can use `Any()` method to generate a random value.
It indicates that value doesn't matter for this test and will never be asserted on (but required for API, for example).
### testcontext package
There's no need to create your own `context.Context` via `context.WithTimeout`; use `testcontext.New(t)` instead. It returns a test-scoped `context.Context`, memoized per `*testing.T` and canceled on test end or timeout.
### taskpoller package
For end-to-end testing, consider using `taskpoller.TaskPoller` to handle workflow tasks. This is
useful when you need full control over the worker behavior in a way that the SDK cannot provide;
or if there's no SDK support for that API available yet.
You'll find a fully initialized task poller in any functional test suite, look for `s.TaskPoller`.
_NOTE: The previous `testcore.TaskPoller` has been deprecated and should not be used in new code._
### testhooks package
The `testhooks` package injects test-specific behavior into production code paths that are otherwise
difficult to test. This is a **last resort** - prefer mocking and dependency injection when possible.
**Example:**
The UpdateWithStart API has a race window between releasing a lock and starting a workflow where
another request could create the same workflow first. The `UpdateWithStartInBetweenLockAndStart`
hook lets tests inject a callback at this exact point, making it possible to reliably test
conflict handling.
_NOTE: Tests using testhooks must be run with `-tags=test_dep`._
### softassert package
`softassert.That` is a "soft" assertion that logs an error if the given condition is false.
It is useful to highlight invariant violations in production code.
It is *not* a substitute for regular error handling, validation, or control flow.
In functional tests, a failed soft assertion will not stop the test execution immediately, but it
will ultimately fail the test.
### protorequire package
Use `protorequire.ProtoEqual` to compare proto messages with proto semantics.
Prefer a single `ProtoEqual` call over asserting fields one-by-one, since it catches unexpected field changes and keeps the expected value next to the assertion.
To ignore specific fields on the top-level message (e.g. non-deterministic timestamps), pass `protorequire.IgnoreFields`:
```go
protorequire.ProtoEqual(t, expected, actual,
protorequire.IgnoreFields(
"execution_duration",
"schedule_time",
),
)
```
### historyrequire package
`historyrequire` has assertions to verify workflow event histories.
Use `EqualHistoryEvents` to assert the full event sequence:
```go
events := env.GetHistory(env.Namespace().String(), workflowExecution)
s.EqualHistoryEvents(`
1 WorkflowExecutionStarted
2 WorkflowTaskScheduled {"Attempt": 1}
3 WorkflowTaskStarted
4 WorkflowTaskCompleted
5 WorkflowExecutionCompleted`, events)
```
Optional inline JSON (e.g. `{"Attempt": 1}`) can be used to assert on specific attributes.
Use `ContainsHistoryEvents` when you only care about a particular segment:
```go
s.ContainsHistoryEvents(`
4 WorkflowTaskFailed {"Identity": "worker-1"}
5 WorkflowTaskScheduled
6 WorkflowTaskStarted`, events)
```
Use `RequireHistoryEvent` when you only care about a single event type:
```go
completed := s.RequireHistoryEvent(events, enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED)
require.Equal(t, "expected-result", completed.GetWorkflowExecutionCompletedEventAttributes().Result)
```
Or use `RequireNoHistoryEvent` when you expect no event of a given type to be present.
### Test Cluster
Use `testcore.NewEnv(t)` to create a test environment with access to a Temporal cluster for end-to-end testing.
```go
func (s* TestMyFeatureSuite) func TestXYZ(t *testing.T) {
s := testcore.NewEnv(t)
// ...
}
```
Note that each test has its own namespace (`s.Namespace()`) for isolation.
> **Note:** The legacy `FunctionalTestBase` (using testify's `suite`) has been deprecated for new tests.
## OpenTelemetry (OTEL)
To debug your test by analysing observability traces, set the following environment variables:
```bash
export OTEL_BSP_SCHEDULE_DELAY=100
export OTEL_EXPORTER_OTLP_TRACES_INSECURE=true
export OTEL_TRACES_EXPORTER=otlp
export TEMPORAL_OTEL_DEBUG=true
```
And have an OTEL collector running, such as Grafana Tempo (`make start-dependencies`).
See [tracing.md](../../docs/development/tracing.md) for more details.
## Code coverage
You'll find the code coverage reporting in Codecov: https://app.codecov.io/gh/temporalio/temporal.
Consider installing the [Codecov Browser Extension](https://docs.codecov.com/docs/the-codecov-browser-extension)
to see code coverage directly in GitHub PRs.