Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions examples/components/workflows/redis.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
spec:
type: state.redis
version: v1
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: ""
- name: actorStateStore
value: "true"
5 changes: 5 additions & 0 deletions examples/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@
<artifactId>dapr-sdk-actors</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk-workflows</artifactId>
<version>${dapr.sdk-workflows.version}</version>
</dependency>
<dependency>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright 2023 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
limitations under the License.
*/

package io.dapr.examples.unittesting;

import com.microsoft.durabletask.Task;
import com.microsoft.durabletask.TaskCanceledException;
import io.dapr.workflows.Workflow;
import io.dapr.workflows.WorkflowContext;
import io.dapr.workflows.WorkflowStub;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.slf4j.Logger;

import java.time.Duration;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.startsWith;
import static org.mockito.Mockito.mock;

/**
* 1. Build and install jars:
* mvn clean install
* 2. cd [repo root]/examples
* 3. Run the test code:
* java -jar target/dapr-java-sdk-examples-exec.jar \
* org.junit.platform.console.ConsoleLauncher --select-class=io.dapr.examples.unittesting.DaprWorkflowExampleTest
*/
public class DaprWorkflowExampleTest {
private static final String timeoutWorkflow = "DemoWorkflowTimeout";
private static final String noTimeoutWorkflow = "DemoWorkflowNoTimeout";
private static final String workflowDefaultId = "demo-workflow-123";

private class DemoWorkflow extends Workflow {

@Override
public WorkflowStub create() {
return ctx -> {
String name = ctx.getName();
String id = ctx.getInstanceId();
try {
ctx.waitForExternalEvent(name, Duration.ofMillis(100)).await();
} catch (TaskCanceledException e) {
ctx.getLogger().warn("Timed out");
}
String output = name + ":" + id;
ctx.complete(output);
};
}
}

@Test
public void testWorkflow() {
String name = noTimeoutWorkflow;
String id = workflowDefaultId;
WorkflowContext mockContext = createMockContext(name, id);

new DemoWorkflow().run(mockContext);

String expectedOutput = name + ":" + id;
Mockito.verify(mockContext, Mockito.times(1)).complete(expectedOutput);
}

@Test
public void testWorkflowWaitForEventTimeout() {
WorkflowContext mockContext = createMockContext(timeoutWorkflow, workflowDefaultId);
Logger mockLogger = mock(Logger.class);
Mockito.doReturn(mockLogger).when(mockContext).getLogger();

new DemoWorkflow().run(mockContext);

Mockito.verify(mockLogger, Mockito.times(1)).warn("Timed out");
}

@Test
public void testWorkflowWaitForEventNoTimeout() {
WorkflowContext mockContext = createMockContext(noTimeoutWorkflow, workflowDefaultId);
Logger mockLogger = mock(Logger.class);
Mockito.doReturn(mockLogger).when(mockContext).getLogger();

new DemoWorkflow().run(mockContext);

Mockito.verify(mockLogger, Mockito.times(0)).warn(anyString());
}

private WorkflowContext createMockContext(String name, String id) {
WorkflowContext mockContext = mock(WorkflowContext.class);

Mockito.doReturn(name).when(mockContext).getName();
Mockito.doReturn(id).when(mockContext).getInstanceId();
Mockito.doReturn(mock(Task.class))
.when(mockContext).waitForExternalEvent(startsWith(noTimeoutWorkflow), any(Duration.class));
Mockito.doThrow(TaskCanceledException.class)
.when(mockContext).waitForExternalEvent(startsWith(timeoutWorkflow), any(Duration.class));

return mockContext;
}
}
139 changes: 137 additions & 2 deletions examples/src/main/java/io/dapr/examples/unittesting/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ cd examples
```

### Understanding the code
This example will simulate an application code via the App class:

#### Example App Test
This example, found in `DaprExampleTest.java`, will simulate an application code via the App class:

```java
private static final class MyApp {
Expand Down Expand Up @@ -115,7 +117,7 @@ The second test uses a mock implementation of the factory method and checks the
```


### Running the example
##### Running the example
<!-- STEP
name: Check state example
expected_stdout_lines:
Expand Down Expand Up @@ -159,4 +161,137 @@ Test run finished after 1210 ms
[ 0 tests aborted ]
[ 2 tests successful ]
[ 0 tests failed ]
```

#### Example Workflow Test
This example, found in `DaprWorkflowExampleTest.java`, shows how to mock and test a dapr workflow:

```java
private class DemoWorkflow extends Workflow {

@Override
public void run(WorkflowContext ctx) {
String name = ctx.getName();
String id = ctx.getInstanceId();
try {
ctx.waitForExternalEvent("myEvent", Duration.ofSeconds(10)).await();
} catch (TaskCanceledException e) {
ctx.getLogger().warn("Timed out");
}
String output = name + ":" + id;
ctx.complete(output);
}
}
```

The example provides its own workflow, but for a production system you would want to import and use your own workflow. The goal of unit testing a workflow is to ensure that the business logic functions as expected. For our example that is these two sections:

```java
String output = name + ":" + id;
```

```java
} catch (TaskCanceledException e) {
ctx.getLogger().warn("Timed out");
}
```


The first test validates the output of our workflow by mocking the `WorkflowContext` and verifying the `.complete` method:
```java
@Test
public void testWorkflow() {
WorkflowContext mockContext = Mockito.mock(WorkflowContext.class);
String name = "DemoWorkflow";
String id = "my-workflow-123";

Mockito.when(mockContext.getName()).thenReturn(name);
Mockito.when(mockContext.getInstanceId()).thenReturn(id);

new DemoWorkflow().run(mockContext);

String expectedOutput = name + ":" + id;
Mockito.verify(mockContext, times(1)).complete(expectedOutput);
}
```

The second test validates the `catch` block of our workflow by throwing the expected exception from our mock and verifying the expected call on our mock `Logger`:
```java
@Test
public void testWorkflowWaitForEventTimeout() {
WorkflowContext mockContext = Mockito.mock(WorkflowContext.class);
Logger mockLogger = Mockito.mock(Logger.class);

Mockito.when(mockContext.getLogger()).thenReturn(mockLogger);
Mockito.when(mockContext.waitForExternalEvent(anyString(),any(Duration.class)))
.thenThrow(TaskCanceledException.class);

new DemoWorkflow().run(mockContext);

Mockito.verify(mockLogger, times(1)).warn("Timed out");
}
```

The third test is similar but validates the inverse of the test above, ensuring that a code path we do not expect to be triggered is indeed avoided:
```java
@Test
public void testWorkflowWaitForEventNoTimeout() {
WorkflowContext mockContext = Mockito.mock(WorkflowContext.class);
Logger mockLogger = Mockito.mock(Logger.class);

Mockito.when(mockContext.getLogger()).thenReturn(mockLogger);

new DemoWorkflow().run(mockContext);

Mockito.verify(mockLogger, times(0)).warn(anyString());
}
```


##### Running the example
<!-- STEP
name: Check state example
expected_stdout_lines:
- "[ 3 tests found ]"
- "[ 0 tests skipped ]"
- "[ 3 tests started ]"
- "[ 0 tests aborted ]"
- "[ 3 tests successful ]"
- "[ 0 tests failed ]"
background: true
sleep: 5
-->

Run this example with the following command:
```bash
java -jar target/dapr-java-sdk-examples-exec.jar org.junit.platform.console.ConsoleLauncher --select-class=io.dapr.examples.unittesting.DaprWorkflowExampleTest
```

<!-- END_STEP -->

After running, Junit should print the output as follows:

```txt
├─ JUnit Jupiter ✔
│ └─ DaprWorkflowExampleTest ✔
│ ├─ testWorkflowWaitForEventTimeout() ✔
│ ├─ testWorkflowWaitForEventNoTimeout() ✔
│ └─ testWorkflow() ✔
└─ JUnit Vintage ✔

Test run finished after 815 ms
[ 3 containers found ]
[ 0 containers skipped ]
[ 3 containers started ]
[ 0 containers aborted ]
[ 3 containers successful ]
[ 0 containers failed ]
[ 3 tests found ]
[ 0 tests skipped ]
[ 3 tests started ]
[ 0 tests aborted ]
[ 3 tests successful ]
[ 0 tests failed ]

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright 2023 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
limitations under the License.
*/

package io.dapr.examples.workflows;

import com.microsoft.durabletask.TaskCanceledException;
import io.dapr.workflows.Workflow;
import io.dapr.workflows.WorkflowStub;

import java.time.Duration;

/**
* Implementation of the DemoWorkflow for the server side.
*/
public class DemoWorkflow extends Workflow {

@Override
public WorkflowStub create() {
return ctx -> {
ctx.getLogger().info("Starting Workflow: " + ctx.getName());
ctx.getLogger().info("Instance ID: " + ctx.getInstanceId());
ctx.getLogger().info("Waiting for event: 'myEvent'...");
try {
ctx.waitForExternalEvent("myEvent", Duration.ofSeconds(10)).await();
ctx.getLogger().info("Received!");
} catch (TaskCanceledException e) {
ctx.getLogger().warn("Timed out");
ctx.getLogger().warn(e.getMessage());
}
ctx.complete("finished");
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2023 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
limitations under the License.
*/

package io.dapr.examples.workflows;

import io.dapr.workflows.client.DaprWorkflowClient;

import java.util.concurrent.TimeUnit;

/**
* For setup instructions, see the README.
*/
public class DemoWorkflowClient {

/**
* The main method.
* @param args Input arguments (unused).
* @throws InterruptedException If program has been interrupted.
*/
public static void main(String[] args) throws InterruptedException {
DaprWorkflowClient client = new DaprWorkflowClient();

try (client) {
System.out.println("*****");
String instanceId = client.scheduleNewWorkflow(DemoWorkflow.class);
System.out.printf("Started new workflow instance with random ID: %s%n", instanceId);

System.out.println("Sleep and allow this workflow instance to timeout...");
TimeUnit.SECONDS.sleep(10);

System.out.println("*****");
String instanceToTerminateId = "terminateMe";
client.scheduleNewWorkflow(DemoWorkflow.class, null, instanceToTerminateId);
System.out.printf("Started new workflow instance with specified ID: %s%n", instanceToTerminateId);

TimeUnit.SECONDS.sleep(5);
System.out.println("Terminate this workflow instance manually before the timeout is reached");
client.terminateWorkflow(instanceToTerminateId, null);
System.out.println("*****");
}

System.out.println("Exiting DemoWorkflowClient.");
System.exit(0);
}
}
Loading