iT邦幫忙

2025 iThome 鐵人賽

DAY 23
0
Software Development

Temporal 開發指南:掌握 Workflow as Code 打造穩定可靠的分散式流程系列 第 23

Day23 - Temporal Testing (一):Workflow.sleep(), Activity.getExecutionContext()

  • 分享至 

  • xImage
  •  

本篇重點:使用 Temporal Testing SDK 跑起一個 Temporal 服務模擬,並了解如何開始測試 Worker、Workflow 與 Activity。

1. 為什麼需要測 Temporal 流程

  • 狀態機與分支很多:Workflow 會有 wait/sleep/retry/signal 等互動,僅依靠一般的單元測試很難覆蓋。
  • 長時間行為可快轉測:計時器可能是數秒鐘到數小時;在測試中可用虛擬時間一秒快轉跳過。
  • 可再現與可回溯:透過 event history 能穩定重放測試

2. Temporal 測試總覽

2.1 測試範圍

  • 單元測試(Unit):純邏輯的 Activity
  • 內嵌整合(Embedded Integration):本篇的主要內容。用 TestWorkflowEnvironment 跑 in‑memory service/worker/client。
  • 端到端(E2E):連接真實 Temporal Server(docker/temporalite),模擬實際佈署。

2.2 測試框架

  • io.temporal.testing.TestWorkflowEnvironment:Temporal 提供了一個測試框架,用於簡化 Workflow 單元測試和整合測試。該測試框架提供了一個 TestWorkflowEnvironment 類,其中包含 Temporal 服務的記憶體實作,支援快轉跳過時間,輕鬆測試長時間運行的 Workflow。
  • io.temporal.testing.TestActivityEnvironment:用來獨立測試 Activity

2.3 測試套件依賴 (本篇以 Gradle 載入)

plugins {
  id 'java'
}

repositories {
  mavenCentral()
}

dependencies {
  implementation 'io.temporal:temporal-sdk:1.31.0'
  // 測試套件
  testImplementation 'io.temporal:temporal-testing:1.31.0'

  testImplementation 'org.junit.jupiter:junit-jupiter:5.10.3'
}

test {
  useJUnitPlatform()
}

3. Workflow 測試:Signal 更新的流程

3.1 Workflow 及 Activity 介面與實作

@WorkflowInterface
public interface GreetingWorkflow {
  // Workflow 入口方法:必須是 deterministic,且可被重播
  @WorkflowMethod
  String greet(String name);

  // Signal:在執行中由外部傳入事件以更新 workflow 狀態
  @SignalMethod
  void updateGreeting(String greeting);

  // Query:讀取 workflow 目前狀態,不改變狀態
  @QueryMethod
  String getGreeting();
}
@ActivityInterface
public interface GreetingActivities {
  // Activity:可含 I/O 與非決定性操作
  @ActivityMethod
  String composeGreeting(String greeting, String name);
}
public class GreetingWorkflowImpl implements GreetingWorkflow {
  private String currentGreeting = "Hello";

  // 以固定 timeout 建立 Activity stub;由 Workflow 執行循環呼叫
  private final GreetingActivities activities =
      Workflow.newActivityStub(
          GreetingActivities.class,
          ActivityOptions.newBuilder()
              .setStartToCloseTimeout(Duration.ofSeconds(5))
              .build());

  @Override
  public String greet(String name) {
    
    // *** 測試時將快轉跨越這段 sleep ***
    Workflow.sleep(Duration.ofHours(1));
    
    return activities.composeGreeting(currentGreeting, name);
  }

  @Override
  public void updateGreeting(String greeting) {
    // Signal handler:更新 workflow 內部狀態
    this.currentGreeting = greeting;
  }

  @Override
  public String getGreeting() {
    // Query handler:回報目前狀態
    return currentGreeting;
  }
}
public class GreetingActivitiesImpl implements GreetingActivities {
  @Override
  public String composeGreeting(String greeting, String name) {
    return String.format("%s, %s!", greeting, name);
  }
}

3.2 內嵌整合測試:TestWorkflowEnvironment 啟動 Worker 與 Workflow/Activity,搭配時間快轉驗證互動

public class GreetingWorkflowTest {
  private static final String TASK_QUEUE = "greeting-task-queue";

  // in-memory Temporal service 與 client
  private TestWorkflowEnvironment testEnv;
  private WorkflowClient client;

  @BeforeEach
  void setUp() {
    // 建立 in-memory Temporal service 與 client
    testEnv = TestWorkflowEnvironment.newInstance();
    // 建立 worker 並註冊 workflow / activities 實作
    Worker worker = testEnv.newWorker(TASK_QUEUE);
    worker.registerWorkflowImplementationTypes(GreetingWorkflowImpl.class);
    // 以匿名類別提供 Activity 實作,避免額外測試替身類別
    worker.registerActivitiesImplementations(new GreetingActivities() {
      @Override
      public String composeGreeting(String greeting, String name) {
        return String.format("%s, %s!", greeting, name);
      }
    });
    // 啟動內嵌服務
    testEnv.start();
    client = testEnv.getWorkflowClient();
  }

  @AfterEach
  void tearDown() {
    testEnv.close();
  }

  @Test
  void greet_withVirtualTime_andSignal() {
    GreetingWorkflow workflow = client.newWorkflowStub(
        GreetingWorkflow.class,
        WorkflowOptions.newBuilder().setTaskQueue(TASK_QUEUE).build());

    // 非同步啟動 workflow,讓我們能在進行中送 Signal
    WorkflowClient.start(() -> workflow.greet("Alice"));
    // *** greet 內有 Workflow.sleep 會被快轉跳過 ***

    // 立刻用 Signal 更新狀態
    workflow.updateGreeting("Howdy");

    // 取得結果並驗證
    String result = WorkflowStub.fromTyped(workflow).getResult(String.class);
    assertEquals("Howdy, Alice!", result);
  }
}

3.3 測試重點

  • WorkflowClient.start 非同步啟動後即可在送 Signal 或做 Query
  • 由於使用 TestWorkflowEnvironment,所以 Workflow.sleep(Duration.ofHours(1)) 是快轉通過不會卡時間的。

4. Activity 測試:需要 SDK 情境(Context Info 最小例)

Activity 需取用 ActivityExecutionContext.getInfo() 資訊(例如 workflowId),就需要用到 SDK 來進行驗證。

4.1 以下展示如何在 Activity 取用 workflowId,並於測試中斷言

public class GreetingActivitySdkTest {
  private static final String TASK_QUEUE = "greeting-task-queue";

  private TestWorkflowEnvironment testEnv;
  private WorkflowClient client;

  @BeforeEach
  void setUp() {
    testEnv = TestWorkflowEnvironment.newInstance();
    Worker worker = testEnv.newWorker(TASK_QUEUE);
    worker.registerWorkflowImplementationTypes(GreetingWorkflowImpl.class);

    // Activity 實作
    worker.registerActivitiesImplementations(new GreetingActivities() {
      @Override
      public String composeGreeting(String greeting, String name) {
        // 讀取 workflowId 並組合回傳字串
        String wfId = Activity.getExecutionContext().getInfo().getWorkflowId();
        return String.format("%s, %s! (%s)", greeting, name, wfId);
      }
    });

    testEnv.start();
    client = testEnv.getWorkflowClient();
  }

  @AfterEach
  void tearDown() {
    testEnv.close();
  }

  @Test
  void activity_can_access_workflowId_via_context() {
    String expectedWorkflowId = "wf-ctx-1";
    GreetingWorkflow workflow = client.newWorkflowStub(
        GreetingWorkflow.class,
        WorkflowOptions.newBuilder()
            .setTaskQueue(TASK_QUEUE)
            .setWorkflowId(expectedWorkflowId)
            .build());

    // 非同步啟動(自動快轉跳過 Workflow.sleep 計時器)
    WorkflowClient.start(() -> workflow.greet("Bob"));

    // 驗證 Activity 的回傳包含 workflowId(透過 SDK context 取得)
    String result = WorkflowStub.fromTyped(workflow).getResult(String.class);

    assertEquals("Hello, Bob! (" + expectedWorkflowId + ")", result);
  }
}

4.2 測試重點

  • 驗證 Activity 能從 SDK context 讀取 workflowId,並正確帶入邏輯及輸出

結語

我們可以透過 Temporal 測試框架用最小成本覆蓋 Workflow 與 Activity 的核心行為,快速得到穩定且可重現的測試回饋。


上一篇
Day22 - Temporal Workflow Versioning 流程版本管理
下一篇
Day24 - Temporal Testing(二):Workflow Versioning、Workflow.await、Heartbeat/Cancel
系列文
Temporal 開發指南:掌握 Workflow as Code 打造穩定可靠的分散式流程24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言