本篇的測試主題為:
在 Activity 內以 doNotCompleteOnReturn()
延後完成,由外部(或背景執行緒)透過 ActivityCompletionClient
回傳結果。
@ActivityInterface
public interface AsyncAct {
@ActivityMethod
String waitExternal(String id);
}
class AsyncActImpl implements AsyncAct {
private final ActivityCompletionClient completionClient;
AsyncActImpl(ActivityCompletionClient completionClient) {
this.completionClient = completionClient;
}
@Override
public String waitExternal(String id) {
// 標記此 Activity 當前不回傳結果,稍後由外部完成
ActivityExecutionContext ctx = Activity.getExecutionContext();
ctx.doNotCompleteOnReturn();
// 擷取 task token,供外部 completion client 使用
byte[] token = ctx.getTaskToken();
// 模擬外部系統 callback:取得 token 後於背景完成 Activity
new Thread(() -> completionClient.complete(token, "READY-" + id)).start();
return null;
}
}
@WorkflowInterface
public interface AsyncWf {
@WorkflowMethod
String run(String id);
}
class AsyncWfImpl implements AsyncWf {
@Override
public String run(String id) {
AsyncAct act = Workflow.newActivityStub(
AsyncAct.class,
ActivityOptions.newBuilder()
.setStartToCloseTimeout(Duration.ofSeconds(10))
.build());
return act.waitExternal(id);
}
}
public class AsyncCompletionTest {
@Test
void async_activity_completion() {
try (TestWorkflowEnvironment env = TestWorkflowEnvironment.newInstance()) {
Worker w = env.newWorker("async-q"); // 測試專用 task queue
ActivityCompletionClient acc = env.getWorkflowClient().newActivityCompletionClient(); // 供外部完成
w.registerWorkflowImplementationTypes(AsyncWfImpl.class);
// 以匿名類別提供 async activity 實作
w.registerActivitiesImplementations(new AsyncActImpl(acc));
env.start();
WorkflowClient c = env.getWorkflowClient();
AsyncWf wf = c.newWorkflowStub(AsyncWf.class,
WorkflowOptions.newBuilder().setTaskQueue("async-q").build());
// 由 activity completion client 回填 "READY-X1"
String out = wf.run("X1");
assertEquals("READY-X1", out);
}
}
}
doNotCompleteOnReturn()
後,方法回傳值不會完成 Activity,真正結果由 ActivityCompletionClient.complete(token, value)
回填。Activity.getExecutionContext().getTaskToken()
,確保一次性、可辨識該 Activity 嘗試。completeExceptionally(token, Throwable)
驗證 Workflow 端會收到 ApplicationFailure
(或對應例外),避免吞例外。若偏好以 Extension 管理生命週期,可用 TestWorkflowExtension
:
面向 | TestWorkflowExtension | TestWorkflowEnvironment |
---|---|---|
定位 | JUnit 5 Extension 封裝 | 原生測試環境 API |
生命週期 | 由 JUnit 管理啟停 | 由測試程式手動啟停 |
樣板碼 | 少,builder 註冊即可 | 多,需註冊/start() /close() |
客製化彈性 | 較少,但可取用底層 env | 最高,便於進階組態/多 worker |
測試框架綁定 | 綁 JUnit 5 | 無(框架無關) |
適用場景 | 一般單元/內嵌整合,追求易用 | 進階情境、細緻控制、非 JUnit |
優點 | 易用、穩定、少樣板、避免漏關閉 | 完整掌控、彈性強、可程式化流程 |
缺點 | 彈性略低、需 JUnit 5 | 樣板多、易因順序/關閉疏漏出錯 |
@WorkflowInterface
interface GreetingWorkflow {
@WorkflowMethod
String greet(String name);
}
class GreetingWorkflowImpl implements GreetingWorkflow {
@Override
public String greet(String name) {
return "Hello, " + name + "!";
}
}
public class ExtensionStyleTest {
@RegisterExtension
public static final TestWorkflowExtension ext = TestWorkflowExtension.newBuilder()
.registerWorkflowImplementationTypes(GreetingWorkflowImpl.class) // 註冊 workflow 實作
// 自動使用預設的 task queue
.setDoNotStart(false) // 測試前自動啟動
.build();
// 1. 注入 TestWorkflowEnvironment 與 WorkflowClient,手動建立 stub(彈性高)
@Test
void run_with_env_and_client(TestWorkflowEnvironment env, WorkflowClient client) {
GreetingWorkflow wf = client.newWorkflowStub(
GreetingWorkflow.class,
WorkflowOptions.newBuilder().setTaskQueue("default").build()
);
String res = wf.greet("Zoe");
assertEquals("Hello, Zoe!", res);
}
// 2. 直接注入 Workflow stub(最省樣板, 自動使用預設的 task queue)
@Test
void run_with_injected_stub(GreetingWorkflow wf) {
String res = wf.greet("Ada");
assertEquals("Hello, Ada!", res);
}
}
TestWorkflowEnvironment
、WorkflowClient
,也可直接注入 Workflow Stub,減少樣板程式碼。TestWorkflowExtension
不提供自訂 task queue;注入的 stub 已綁定內部私有 queue。若手動用 WorkflowClient
建 stub,須指定與內部一致的 queue,否則 Worker 收不到任務。啟動 Temporal 有多種方式,基本上程式碼不需要變動,只要搭配慣用的 CI 設定,再導入開發測試流程即可,以下僅列兩種方法。
temporal server start-dev
docker-compose-postgres.yml
以供參考version: "3.5"
services:
#postgresql: ...
temporal:
container_name: temporal
depends_on:
- postgresql
environment:
- DB=postgres12
- DB_PORT=5432
- POSTGRES_USER=temporal
- POSTGRES_PWD=temporal
- POSTGRES_SEEDS=postgresql
- DYNAMIC_CONFIG_FILE_PATH=config/dynamicconfig/development-sql.yaml
- TEMPORAL_ADDRESS=temporal:7233
- TEMPORAL_CLI_ADDRESS=temporal:7233
image: temporalio/auto-setup:${TEMPORAL_VERSION}
networks:
- temporal-network
ports:
- 7233:7233 # 啟動之後,可以透過 7233 連上 server
volumes:
- ./dynamicconfig:/etc/temporal/config/dynamicconfig
# temporal-admin-tools: ...
temporal-ui:
container_name: temporal-ui
depends_on:
- temporal
environment:
- TEMPORAL_ADDRESS=temporal:7233
- TEMPORAL_CORS_ORIGINS=http://localhost:3000
image: temporalio/ui:${TEMPORAL_UI_VERSION}
networks:
- temporal-network
ports:
- 8080:8080 # 啟動之後,可以透過 8080 連上 ui
networks:
temporal-network:
driver: bridge
name: temporal-network
最後一篇 Temporal Testing 處理了非同步 Activity Completion 測試、分享搭配 JUnit 5 使用 TestWorkflowExtension
簡化的作法,以及如何建立 E2E 測試環境。