• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

temporalio / sdk-java / #351

04 Aug 2026 11:58PM UTC coverage: 67.781% (-0.5%) from 68.245%
#351

push

github

web-flow
Add dev server downloader & runner to testing package (#2982)

7311 of 12876 branches covered (56.78%)

Branch coverage included in aggregate %.

331 of 703 new or added lines in 12 files covered. (47.08%)

28 existing lines in 13 files now uncovered.

30207 of 42476 relevant lines covered (71.12%)

0.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

7.41
/temporal-testing/src/main/java/io/temporal/testing/TestWorkflowEnvironment.java
1
package io.temporal.testing;
2

3
import io.temporal.api.common.v1.WorkflowExecution;
4
import io.temporal.api.enums.v1.IndexedValueType;
5
import io.temporal.api.nexus.v1.Endpoint;
6
import io.temporal.client.ActivityClient;
7
import io.temporal.client.WorkflowClient;
8
import io.temporal.common.Experimental;
9
import io.temporal.common.WorkflowExecutionHistory;
10
import io.temporal.serviceclient.OperatorServiceStubs;
11
import io.temporal.serviceclient.WorkflowServiceStubs;
12
import io.temporal.worker.Worker;
13
import io.temporal.worker.WorkerFactory;
14
import io.temporal.worker.WorkerOptions;
15
import java.io.Closeable;
16
import java.time.Duration;
17
import java.util.concurrent.TimeUnit;
18
import javax.annotation.Nonnull;
19
import javax.annotation.Nullable;
20

21
/**
22
 * TestWorkflowEnvironment provides workflow unit testing capabilities.
23
 *
24
 * <p>Testing the workflow code is hard as it might be potentially very long-running. The included
25
 * in-memory implementation of the Temporal service supports <b>an automatic time skipping</b>.
26
 * Anytime a workflow under the test as well as the unit test code are waiting on a timer (or sleep)
27
 * the internal service time is automatically advanced to the nearest time that unblocks one of the
28
 * waiting threads. This way a workflow that runs in production for months is unit tested in
29
 * milliseconds. Here is an example of a test that executes in a few milliseconds instead of over
30
 * two hours that are needed for the workflow to complete:
31
 *
32
 * <pre><code>
33
 *   public class SignaledWorkflowImpl implements SignaledWorkflow {
34
 *     private String signalInput;
35
 *
36
 *    {@literal @}Override
37
 *     public String workflow1(String input) {
38
 *       Workflow.sleep(Duration.ofHours(1));
39
 *       Workflow.await(() -&gt; signalInput != null);
40
 *       Workflow.sleep(Duration.ofHours(1));
41
 *       return signalInput + "-" + input;
42
 *     }
43
 *
44
 *    {@literal @}Override
45
 *     public void processSignal(String input) {
46
 *       signalInput = input;
47
 *    }
48
 *  }
49
 *
50
 * {@literal @}Test
51
 *  public void testSignal() throws ExecutionException, InterruptedException {
52
 *    TestWorkflowEnvironment testEnvironment = TestWorkflowEnvironment.newInstance();
53
 *
54
 *    // Creates a worker that polls tasks from the service owned by the testEnvironment.
55
 *    Worker worker = testEnvironment.newWorker(TASK_QUEUE);
56
 *    worker.registerWorkflowImplementationTypes(SignaledWorkflowImpl.class);
57
 *    worker.start();
58
 *
59
 *    // Creates a WorkflowClient that interacts with the server owned by the testEnvironment.
60
 *    WorkflowClient client = testEnvironment.getWorkflowClient();
61
 *    SignaledWorkflow workflow = client.newWorkflowStub(SignaledWorkflow.class);
62
 *
63
 *    // Starts a workflow execution
64
 *    CompletableFuture<String> result = WorkflowClient.execute(workflow::workflow1, "input1");
65
 *
66
 *    // The sleep forwards the service clock for 65 minutes without blocking.
67
 *    // This ensures that the signal is sent after the one hour sleep in the workflow code.
68
 *    testEnvironment.sleep(Duration.ofMinutes(65));
69
 *    workflow.processSignal("signalInput");
70
 *
71
 *    // Blocks until workflow is complete. Workflow sleep forwards clock for one hour and
72
 *    // this call returns almost immediately.
73
 *    assertEquals("signalInput-input1", result.get());
74
 *
75
 *    // Closes workers and releases in-memory service.
76
 *    testEnvironment.close();
77
 *  }
78
 *
79
 * </code></pre>
80
 */
81
public interface TestWorkflowEnvironment extends Closeable {
82

83
  /** Creates the environment with default options. */
84
  static TestWorkflowEnvironment newInstance() {
85
    return new TestWorkflowEnvironmentInternal(TestEnvironmentOptions.getDefaultInstance());
1✔
86
  }
87

88
  /** Creates the environment with supplied {@code options}. */
89
  static TestWorkflowEnvironment newInstance(TestEnvironmentOptions options) {
90
    return new TestWorkflowEnvironmentInternal(options);
1✔
91
  }
92

93
  /**
94
   * Starts a local Temporal dev server and returns an environment that owns it.
95
   *
96
   * <p>Unlike the in-memory test server, a local dev-server environment does not support time
97
   * skipping.
98
   *
99
   * <pre>{@code
100
   * try (TestWorkflowEnvironment environment = TestWorkflowEnvironment.startLocal()) {
101
   *   Worker worker = environment.newWorker("test-task-queue");
102
   *   // Register implementations and run workflows against the local dev server.
103
   * }
104
   * }</pre>
105
   */
106
  @Experimental
107
  static TestWorkflowEnvironment startLocal() {
NEW
108
    return startLocal(
×
NEW
109
        TestEnvironmentOptions.getDefaultInstance(), TemporalDevServerOptions.getDefaultInstance());
×
110
  }
111

112
  /**
113
   * Starts a local Temporal dev server using the environment namespace and returns an environment
114
   * that owns it. Local dev-server environments do not support time skipping.
115
   */
116
  @Experimental
117
  static TestWorkflowEnvironment startLocal(@Nullable TestEnvironmentOptions testOptions) {
NEW
118
    return startLocal(testOptions, TemporalDevServerOptions.getDefaultInstance());
×
119
  }
120

121
  /**
122
   * Starts a local Temporal dev server with the supplied server options. Local dev-server
123
   * environments do not support time skipping.
124
   */
125
  @Experimental
126
  static TestWorkflowEnvironment startLocal(@Nonnull TemporalDevServerOptions serverOptions) {
NEW
127
    return startLocal(TestEnvironmentOptions.getDefaultInstance(), serverOptions);
×
128
  }
129

130
  /**
131
   * Starts a local Temporal dev server and returns an environment that owns it.
132
   *
133
   * <p>The namespace in {@code testOptions} is authoritative and is created by the dev server.
134
   * Local dev-server environments do not support time skipping.
135
   */
136
  @Experimental
137
  static TestWorkflowEnvironment startLocal(
138
      @Nullable TestEnvironmentOptions testOptions,
139
      @Nonnull TemporalDevServerOptions serverOptions) {
NEW
140
    if (testOptions == null) {
×
NEW
141
      testOptions = TestEnvironmentOptions.getDefaultInstance();
×
142
    }
NEW
143
    TestEnvironmentOptions validated =
×
NEW
144
        TestEnvironmentOptions.newBuilder(testOptions).validateAndBuildWithDefaults();
×
NEW
145
    String namespace = validated.getWorkflowClientOptions().getNamespace();
×
NEW
146
    TemporalDevServer server = TemporalDevServer.start(namespace, serverOptions);
×
147
    try {
NEW
148
      TestEnvironmentOptions localOptions =
×
NEW
149
          TestEnvironmentOptions.newBuilder(validated)
×
NEW
150
              .setUseExternalService(true)
×
NEW
151
              .setUseTimeskipping(false)
×
NEW
152
              .setTarget(server.getTarget())
×
NEW
153
              .build();
×
NEW
154
      return new TestWorkflowEnvironmentInternal(localOptions, server);
×
NEW
155
    } catch (RuntimeException | Error failure) {
×
156
      try {
NEW
157
        server.close();
×
NEW
158
      } catch (RuntimeException | Error cleanupFailure) {
×
NEW
159
        failure.addSuppressed(cleanupFailure);
×
NEW
160
      }
×
NEW
161
      throw failure;
×
162
    }
163
  }
164

165
  /**
166
   * Creates a new Worker instance that is connected to the in-memory test Temporal service.
167
   *
168
   * @param taskQueue task queue to poll.
169
   */
170
  Worker newWorker(String taskQueue);
171

172
  /**
173
   * Creates a new Worker instance that is connected to the in-memory test Temporal service.
174
   *
175
   * @param taskQueue task queue to poll.
176
   */
177
  Worker newWorker(String taskQueue, WorkerOptions options);
178

179
  /** Creates a WorkflowClient that is connected to the in-memory test Temporal service. */
180
  WorkflowClient getWorkflowClient();
181

182
  /** Creates an ActivityClient that is connected to the in-memory test Temporal service. */
183
  ActivityClient getActivityClient();
184

185
  /**
186
   * This time might not be equal to {@link System#currentTimeMillis()} due to time skipping.
187
   *
188
   * @return the current in-memory test Temporal service time in milliseconds or {@link
189
   *     System#currentTimeMillis()} if an external service without time skipping support is used
190
   */
191
  long currentTimeMillis();
192

193
  /**
194
   * Wait until internal test Temporal service time passes the specified duration. This call also
195
   * indicates that workflow time might jump forward (if none of the activities are running) up to
196
   * the specified duration.
197
   *
198
   * <p>This method falls back to {@link Thread#sleep(long)} if an external service without time
199
   * skipping support is used
200
   */
201
  void sleep(Duration duration);
202

203
  /**
204
   * Registers a callback to run after the specified delay according to the test Temporal service
205
   * internal clock.
206
   */
207
  void registerDelayedCallback(Duration delay, Runnable r);
208

209
  /**
210
   * Register a Search Attribute with the server.
211
   *
212
   * @param name Search Attribute name
213
   * @param type Search Attribute type to be used for an elastic search index
214
   * @return {@code true} if the search attribute was registered, false if it was registered already
215
   * @see <a
216
   *     href="https://docs.temporal.io/self-hosted-guide/visibility#create-custom-search-attributes">
217
   *     How to create custom Search Attributes</a>
218
   */
219
  boolean registerSearchAttribute(String name, IndexedValueType type);
220

221
  /**
222
   * Register a Nexus Endpoint with the server.
223
   *
224
   * @param name Nexus Endpoint name
225
   * @param taskQueue Task Queue to be used for the endpoint
226
   * @return Endpoint object
227
   */
228
  Endpoint createNexusEndpoint(String name, String taskQueue);
229

230
  /**
231
   * Delete a Nexus Endpoint on the server.
232
   *
233
   * @param endpoint current endpoint to be deleted
234
   */
235
  void deleteNexusEndpoint(Endpoint endpoint);
236

237
  /**
238
   * @return the in-memory test Temporal service that is owned by this.
239
   * @deprecated use {{@link #getWorkflowServiceStubs()}
240
   */
241
  @Deprecated
242
  WorkflowServiceStubs getWorkflowService();
243

244
  /**
245
   * @return {@link WorkflowServiceStubs} connected to the test server (in-memory or external)
246
   */
247
  WorkflowServiceStubs getWorkflowServiceStubs();
248

249
  /**
250
   * @return {@link io.temporal.serviceclient.OperatorServiceStubs} connected to the test server
251
   */
252
  OperatorServiceStubs getOperatorServiceStubs();
253

254
  String getNamespace();
255

256
  /**
257
   * Currently prints histories of all workflow instances stored in the service. This is useful
258
   * information to print in the case of a unit test failure. A convenient way to achieve this is to
259
   * add the following Rule to a unit test:
260
   *
261
   * <pre><code>
262
   *  {@literal @}Rule
263
   *   public TestWatcher watchman =
264
   *       new TestWatcher() {
265
   *        {@literal @}Override
266
   *         protected void failed(Throwable e, Description description) {
267
   *           System.err.println(testEnvironment.getDiagnostics());
268
   *           testEnvironment.close();
269
   *         }
270
   *       };
271
   * </code></pre>
272
   *
273
   * @return the diagnostic data about the internal service state.
274
   */
275
  String getDiagnostics();
276

277
  /**
278
   * @param execution identifies the workflowId and runId (optionally) to reach the history for
279
   * @return history of the execution
280
   * @deprecated use {@link WorkflowClient#fetchHistory(String, String)}
281
   */
282
  @Deprecated
283
  WorkflowExecutionHistory getWorkflowExecutionHistory(@Nonnull WorkflowExecution execution);
284

285
  /** Calls {@link #shutdownNow()} and {@link #awaitTermination(long, TimeUnit)}. */
286
  @Override
287
  void close();
288

289
  WorkerFactory getWorkerFactory();
290

291
  /** Start all workers created by this factory. */
292
  void start();
293

294
  /** Was {@link #start()} called? */
295
  boolean isStarted();
296

297
  /** Was {@link #shutdownNow()} or {@link #shutdown()} called? */
298
  boolean isShutdown();
299

300
  /** Are all tasks done after {@link #shutdownNow()} or {@link #shutdown()}? */
301
  boolean isTerminated();
302

303
  /**
304
   * Initiates Test Service shutdown. This method is temporarily exposed to solve long poll thread
305
   * shutdown for {@code
306
   * io.temporal.workflow.interceptorsTests.InterceptorExceptionTests#testExceptionOnStart()}. See
307
   * issue: https://github.com/temporalio/sdk-java/issues/608
308
   */
309
  @Deprecated
310
  void shutdownTestService();
311

312
  /**
313
   * Initiates an orderly shutdown in which polls are stopped and already received workflow and
314
   * activity tasks are executed. After the shutdown calls to {@link
315
   * io.temporal.activity.ActivityExecutionContext#heartbeat(Object)} start throwing {@link
316
   * io.temporal.client.ActivityWorkerShutdownException}. Invocation has no additional effect if
317
   * already shut down. This method does not wait for previously received tasks to complete
318
   * execution. Use {@link #awaitTermination(long, TimeUnit)} to do that.
319
   */
320
  void shutdown();
321

322
  /**
323
   * Initiates an orderly shutdown in which polls are stopped and already received workflow and
324
   * activity tasks are attempted to be stopped. This implementation cancels tasks via
325
   * Thread.interrupt(), so any task that fails to respond to interrupts may never terminate. Also,
326
   * after the shutdownNow calls to {@link
327
   * io.temporal.activity.ActivityExecutionContext#heartbeat(Object)} start throwing {@link
328
   * io.temporal.client.ActivityWorkerShutdownException}. Invocation has no additional effect if
329
   * already shut down. This method does not wait for previously received tasks to complete
330
   * execution. Use {@link #awaitTermination(long, TimeUnit)} to do that.
331
   */
332
  void shutdownNow();
333

334
  /**
335
   * Blocks until all tasks have completed execution after a shutdown request, or the timeout
336
   * occurs, or the current thread is interrupted, whichever happens first.
337
   */
338
  void awaitTermination(long timeout, TimeUnit unit);
339
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc