mirror of
https://github.com/temporalio/temporal.git
synced 2026-08-30 18:41:49 -07:00
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.
314 lines
11 KiB
Markdown
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.
|