Temporal Workflow 测试用例编写指南

引言

篇指南旨在为初次使用 Temporal 的开发者提供一个清晰、易懂的 Workflow 测试用例编写框架。Temporal 作为一个强大的工作流编排引擎,其测试框架也同样功能丰富。

为了循序渐进,本次我们先将重点放在 Workflow 的测试用例编写上,关于 Activity 的测试用例,我们将在下一篇文章中详细探讨。

Workflow 测试的两大核心

要成功运行一个 Workflow 测试用例,重点依赖于两个关键部分:测试环境初始化Mock Activity

1. 测试环境初始化

Temporal 提供了一个专用的测试框架,允许你在内存中执行 Workflow,而无需依赖真实的 Temporal 服务。这大大加快了测试速度并简化了环境配置。

一个典型的初始化过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 引入测试包
import (
"testing"
"github.com/stretchr/testify/suite"
"go.temporal.io/sdk/testsuite"
)

// 定义测试套件
type MyWorkflowTestSuite struct {
suite.Suite
testsuite.WorkflowTestSuite

env *testsuite.TestWorkflowEnvironment
}

// SetupTest 会在每个测试用例运行前执行
func (s *MyWorkflowTestSuite) SetupTest() {
s.env = s.NewTestWorkflowEnvironment()
s.env.SetTestTimeout(10 * time.Minute)
s.env.SetWorkflowRunTimeout(60 * time.Minute)
s.env.SetOnTimerFiredListener(func(timerID string) { // 真正的休眠避免快速跳过
s.T().Logf("timerID: %s will sleep 2 seconds", timerID)
time.Sleep(time.Second * 2)
})
}

// TearDownTest 会在每个测试用例运行后执行
func (s *MyWorkflowTestSuite) TearDownTest() {
// 确保所有模拟的调用都已执行
s.env.AssertExpectations(s.T())
}

// 将测试套件注册到 Go 的 testing 框架中
func TestMyWorkflow(t *testing.T) {
suite.Run(t, new(MyWorkflowTestSuite))
}

通过 testsuite.NewTestWorkflowEnvironment(),我们就创建了一个隔离的测试环境 env,后续所有的测试操作都将围绕它进行。

2. Mock Activity

在单元测试中,我们希望专注于测试 Workflow 本身的逻辑,而不是它所调用的 Activity。因此,我们需要对 Activity 进行 Mock(模拟)。

Mock Activity 的好处是:

  • 隔离性:避免外部依赖(如数据库、API 调用)对测试造成干扰。
  • 速度快:直接返回预设结果,无需执行真实的、可能耗时的 Activity 逻辑。
  • 可预测性:可以精确控制 Activity 的返回值和错误,从而测试 Workflow 在不同情况下的行为。

Mock 的基本操作如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func (s *MyWorkflowTestSuite) TestMyWorkflow_Success() {
// 当 MyActivity 被调用时,我们期望它返回 "hello" 并且没有错误
s.env.OnActivity(MyActivity, mock.Anything, "input_param").Return("hello", nil)

// 执行 Workflow
s.env.ExecuteWorkflow(MyWorkflow, "workflow_param")

// 断言 Workflow 是否成功
s.True(s.env.IsWorkflowCompleted())
s.NoError(s.env.GetWorkflowError())

// 断言 Workflow 的返回值
var result string
s.NoError(s.env.GetWorkflowResult(&result))
s.Equal("processed:hello", result)
}

s.env.OnActivity(...) 这行代码就定义了当 MyActivity 被调用时应该发生的行为。

一些实用的测试方法

Temporal 的测试框架提供了一些非常实用的工具函数,可以帮助我们处理各种复杂的测试场景。

  • SetOnTimerFiredListener(listener func(timerID string))
    这个函数用于监听定时器(Timer)触发事件。在测试环境中,为了加速测试执行,Workflow 中的休眠(如 workflow.Sleep)或定时器会被自动跳过。如果你需要模拟真实的等待时间或在定时器触发时执行特定逻辑,这个函数就非常有用。因为测试用例中,workflow的休眠会被跳过,所以需要使用 SetOnTimerFiredListener 来模拟休眠。

    例如,你可以用它来在测试中引入真实的延时:

    1
    2
    3
    4
    s.workflowEnv.SetOnTimerFiredListener(func(timerID string) { // 真正的休眠避免快速跳过
    s.T().Logf("timerID: %s will sleep 2 seconds", timerID)
    time.Sleep(time.Second * 2)
    })

    当你的 Workflow 中有 workflow.NewTimerworkflow.Sleep 逻辑时,你可以通过这个监听器来执行回调,从而模拟更真实的场景或进行相关的断言。

  • SetWorkflowRunTimeout(timeout time.Duration)
    设置 Workflow 的最大运行时间。如果 Workflow 执行时间超过这个阈值,测试将失败。这对于防止测试用例因逻辑错误而无限期阻塞非常有用。

  • RegisterDelayedCallback(callback func(), delay time.Duration)
    注册一个延迟回调。这个函数非常适合用来测试需要与 Workflow 进行异步交互的场景。例如,你可以在 Workflow 启动后的某个时间点,通过这个回调来发送一个 Signal。

  • SignalWorkflow(name string, value interface{})
    向正在运行的 Workflow 发送一个信号(Signal)。这是测试 Workflow 对外部信号响应的核心方法。你可以用它来模拟用户操作、外部事件等。

    1
    2
    3
    4
    5
    // ... 在 ExecuteWorkflow 之后
    // 注册一个回调,在 1 秒后向 Workflow 发送信号
    s.env.RegisterDelayedCallback(func() {
    s.env.SignalWorkflow("my-signal", "signal-data")
    }, time.Second)

常见问题排查

StartToCloseTimeout

当你在运行测试用例时,如果遇到 StartToCloseTimeout 错误,这通常意味着测试用例被阻塞了,Workflow 未能在预期的时间内完成。

最常见的原因之一,是 Workflow 自身的逻辑进入了无法继续执行的状态。一个典型的例子就是等待一个永远不会有新消息的 Channel。

考虑以下 Workflow 代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
func MyWorkflow(ctx workflow.Context, data string) error {
// ... some logic ...

signalCh := workflow.GetSignalChannel(ctx, "my-signal")

// Workflow 将会在这里永远等待,除非有信号进来
var signalData string
signalCh.Receive(ctx, &signalData)

// ... 后续逻辑永远不会被执行 ...
return nil
}

如果在你的测试用例中,启动了这个 Workflow 却忘记了使用 s.env.SignalWorkflow("my-signal", ...) 来发送信号,那么 Workflow 将会永远阻塞在 signalCh.Receive 这一行,最终导致 StartToCloseTimeout 错误。

排查思路

  • 仔细检查 Workflow 代码中所有可能引起等待或阻塞的地方,例如 selector.Select, channel.Receive, workflow.Sleep(如果时间过长)等。
  • 确保你的测试用例覆盖了所有必要的交互,比如发送相应的信号(Signal)或执行回调。

官方资源:Temporal AI 助手

最后,分享一个实用的资源。Temporal 的官方文档网站内嵌了一个 AI 助手。如果你在开发或测试中遇到难以解决的问题,不妨试试向它提问,它有时能提供意想不到的帮助和解决思路。

image-20251030191328733

希望这篇指南能帮助你顺利开启 Temporal 的测试之旅!