Skip to content
Open
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
28 changes: 23 additions & 5 deletions examples/Workflow/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,39 @@ The `OrderProcessingWorkflow.cs` in `Workflows` directory implements the running

This sample also contains a [WorkflowUnitTest](./WorkflowUnitTest) .NET project that utilizes [xUnit](https://xunit.net/) and [Moq](https://github.com/moq/moq) to test the workflow logic.
It works by creating an instance of the `OrderProcessingWorkflow` (defined in the `WorkflowConsoleApp` project), mocking activity calls, and testing the inputs and outputs.
The tests also verify that outputs of the workflow.
The tests verify the outputs of the workflow without requiring a Dapr sidecar, since the `WorkflowContext` itself is mocked.

### Test scenarios

| Test | Concept |
|------|---------|
| `TestSuccessfulOrder` | Mock activity calls, verify inputs and call counts |
| `TestHighCostOrderApproved` | Mock external events (`WaitForExternalEventAsync`) and verify custom status |
| `TestHighCostOrderApprovalTimeout` | Simulate timeouts with `TaskCanceledException` |
| `TestInsufficientInventory` | Branch on activity return value, verify early termination |
| `TestActivityException` | Simulate activity failures with `WorkflowTaskFailedException` |

### Running unit tests

No Dapr sidecar is required. From the repository root:

```sh
dotnet test examples/Workflow/WorkflowUnitTest
```

## Running the console app example

To run the workflow web app locally, two separate terminal windows are required.
In the first terminal window, from the `WorkflowConsoleApp` directory, run the following command to start the program itself:
In the first terminal window, start the dapr sidecar:

```sh
dotnet run
dapr run --app-id wfapp --dapr-grpc-port 50001 --dapr-http-port 3500
```

Next, in a separate terminal window, start the dapr sidecar:
Next, in a seperate terminal window, from the `WorkflowConsoleApp` directory, run the following command to start the program itself:

```sh
dapr run --app-id wfapp --dapr-grpc-port 4001 --dapr-http-port 3500
dotnet run
```

Dapr listens for HTTP requests at `http://localhost:3500`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace WorkflowConsoleApp.Activities;

class UpdateInventoryActivity : WorkflowActivity<PaymentRequest, object>
public class UpdateInventoryActivity : WorkflowActivity<PaymentRequest, object>
{
static readonly string storeName = "statestore";
readonly ILogger logger;
Expand Down Expand Up @@ -53,4 +53,4 @@ await client.SaveStateAsync(

return null;
}
}
}
162 changes: 161 additions & 1 deletion examples/Workflow/WorkflowUnitTest/OrderProcessingTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;
using Dapr.Workflow;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
Expand Down Expand Up @@ -53,6 +54,115 @@ public async Task TestSuccessfulOrder()
Times.Exactly(2));
}

[Fact]
public async Task TestHighCostOrderApproved()
{
// Test payloads
OrderPayload order = new(Name: "Cars", TotalCost: 100000, Quantity: 2);
InventoryResult inventoryResult = new(Success: true, null);
PaymentRequest expectedPaymentRequest = new(It.IsAny<string>(), order.Name, order.Quantity, order.TotalCost);
InventoryRequest expectedInventoryRequest = new(It.IsAny<string>(), order.Name, order.Quantity);

// Mock the call to ReserveInventoryActivity with a total cost exceeding the approval threshold
Mock<WorkflowContext> mockContext = new();
mockContext
.Setup(ctx => ctx.CallActivityAsync<InventoryResult>(nameof(ReserveInventoryActivity), It.IsAny<InventoryRequest>(), It.IsAny<WorkflowTaskOptions>()))
.Returns(Task.FromResult(inventoryResult));
// Approve any approval requests
mockContext
.Setup(ctx => ctx.WaitForExternalEventAsync<ApprovalResult>(It.IsAny<string>(), It.IsAny<TimeSpan>()))
.Returns(Task.FromResult(ApprovalResult.Approved));
mockContext
.Setup(ctx => ctx.CreateReplaySafeLogger<OrderProcessingWorkflow>())
.Returns(NullLogger<OrderProcessingWorkflow>.Instance);

// Run the workflow directly
OrderResult result = await new OrderProcessingWorkflow().RunAsync(mockContext.Object, order);

// Verify that workflow result matches what we expect
Assert.NotNull(result);
Assert.True(result.Processed);

// Verify that ReserveInventoryActivity was called with a specific input
mockContext.Verify(
ctx => ctx.CallActivityAsync<InventoryResult>(nameof(ReserveInventoryActivity), expectedInventoryRequest, It.IsAny<WorkflowTaskOptions>()),
Times.Once());

// Verify that RequestApprovalActivity was called with a specific input
mockContext.Verify(
ctx => ctx.CallActivityAsync(nameof(RequestApprovalActivity), order, It.IsAny<WorkflowTaskOptions>()),
Times.Once());

// Verify that the Approval Request was called with a specific input
mockContext.Verify(
ctx => ctx.WaitForExternalEventAsync<ApprovalResult>("ManagerApproval", TimeSpan.FromSeconds(30)),
Times.Once());

// Verify that the Custom Status was set with a specific message
mockContext.Verify(
ctx => ctx.SetCustomStatus("Waiting for approval"),
Times.Once());

// Verify that ProcessPaymentActivity was called with a specific input
mockContext.Verify(
ctx => ctx.CallActivityAsync(nameof(ProcessPaymentActivity), expectedPaymentRequest, It.IsAny<WorkflowTaskOptions>()),
Times.Once());

// Verify that there were two calls to NotifyActivity
mockContext.Verify(
ctx => ctx.CallActivityAsync(nameof(NotifyActivity), It.IsAny<Notification>(), It.IsAny<WorkflowTaskOptions>()),
Times.Exactly(2));
}

[Fact]
public async Task TestHighCostOrderApprovalTimeout()
{
// Test payloads
OrderPayload order = new(Name: "Cars", TotalCost: 100000, Quantity: 2);
InventoryResult inventoryResult = new(Success: true, null);
InventoryRequest expectedInventoryRequest = new(It.IsAny<string>(), order.Name, order.Quantity);

Mock<WorkflowContext> mockContext = new();
// Mock the call to ReserveInventoryActivity
mockContext
.Setup(ctx => ctx.CallActivityAsync<InventoryResult>(nameof(ReserveInventoryActivity), It.IsAny<InventoryRequest>(), It.IsAny<WorkflowTaskOptions>()))
.Returns(Task.FromResult(inventoryResult));
// Mock a timeout after waiting for approval
mockContext
.Setup(ctx => ctx.WaitForExternalEventAsync<ApprovalResult>(It.IsAny<string>(), It.IsAny<TimeSpan>()))
.Returns(Task.FromException<ApprovalResult>(new TaskCanceledException()));
mockContext
.Setup(ctx => ctx.CreateReplaySafeLogger<OrderProcessingWorkflow>())
.Returns(NullLogger<OrderProcessingWorkflow>.Instance);

// Run the workflow directly
OrderResult result = await new OrderProcessingWorkflow().RunAsync(mockContext.Object, order);

// Verify that workflow result matches what we expect (not processed)
Assert.NotNull(result);
Assert.False(result.Processed);

// Verify that ReserveInventoryActivity was called with a specific input
mockContext.Verify(
ctx => ctx.CallActivityAsync<InventoryResult>(nameof(ReserveInventoryActivity), expectedInventoryRequest, It.IsAny<WorkflowTaskOptions>()),
Times.Once());

// Verify that ProcessPaymentActivity was not called
mockContext.Verify(
ctx => ctx.CallActivityAsync(nameof(ProcessPaymentActivity), It.IsAny<PaymentRequest>(), It.IsAny<WorkflowTaskOptions>()),
Times.Never());

// Verify that UpdateInventoryActivity was not called
mockContext.Verify(
ctx => ctx.CallActivityAsync(nameof(UpdateInventoryActivity), It.IsAny<PaymentRequest>(), It.IsAny<WorkflowTaskOptions>()),
Times.Never());

// Verify that there were two calls to NotifyActivity
mockContext.Verify(
ctx => ctx.CallActivityAsync(nameof(NotifyActivity), It.IsAny<Notification>(), It.IsAny<WorkflowTaskOptions>()),
Times.Exactly(2));
}

[Fact]
public async Task TestInsufficientInventory()
{
Expand Down Expand Up @@ -88,4 +198,54 @@ public async Task TestInsufficientInventory()
ctx => ctx.CallActivityAsync(nameof(NotifyActivity), It.IsAny<Notification>(), It.IsAny<WorkflowTaskOptions>()),
Times.Exactly(2));
}

[Fact]
public async Task TestActivityException()
{
// Test payloads
OrderPayload order = new(Name: "Paperclips", TotalCost: 99.95, Quantity: 10);
PaymentRequest expectedPaymentRequest = new(It.IsAny<string>(), order.Name, order.Quantity, order.TotalCost);
InventoryRequest expectedInventoryRequest = new(It.IsAny<string>(), order.Name, order.Quantity);
InventoryResult inventoryResult = new(Success: true, null);

Mock<WorkflowContext> mockContext = new();
// Mock the call to ReserveInventoryActivity
mockContext
.Setup(ctx => ctx.CallActivityAsync<InventoryResult>(nameof(ReserveInventoryActivity), It.IsAny<InventoryRequest>(), It.IsAny<WorkflowTaskOptions>()))
.Returns(Task.FromResult(inventoryResult));
// Throws a WorkflowTaskFailedException on UpdateInventoryActivity
mockContext
.Setup(ctx => ctx.CallActivityAsync(nameof(UpdateInventoryActivity), It.IsAny<PaymentRequest>(), It.IsAny<WorkflowTaskOptions>()))
.Returns(Task.FromException(new WorkflowTaskFailedException("fail", new WorkflowTaskFailureDetails("type", "message"))));
mockContext
.Setup(ctx => ctx.CreateReplaySafeLogger<OrderProcessingWorkflow>())
.Returns(NullLogger<OrderProcessingWorkflow>.Instance);

// Run the workflow directly
OrderResult result = await new OrderProcessingWorkflow().RunAsync(mockContext.Object, order);

// Verify that workflow result matches what we expect (not processed)
Assert.NotNull(result);
Assert.False(result.Processed);

// Verify that ReserveInventoryActivity was called with a specific input
mockContext.Verify(
ctx => ctx.CallActivityAsync<InventoryResult>(nameof(ReserveInventoryActivity), expectedInventoryRequest, It.IsAny<WorkflowTaskOptions>()),
Times.Once());

// Verify that ProcessPaymentActivity was called with a specific input
mockContext.Verify(
ctx => ctx.CallActivityAsync(nameof(ProcessPaymentActivity), expectedPaymentRequest, It.IsAny<WorkflowTaskOptions>()),
Times.Once());

// Verify that UpdateInventoryActivity was called with a specific input
mockContext.Verify(
ctx => ctx.CallActivityAsync(nameof(UpdateInventoryActivity), expectedPaymentRequest, It.IsAny<WorkflowTaskOptions>()),
Times.Once());

// Verify that there were two calls to NotifyActivity
mockContext.Verify(
ctx => ctx.CallActivityAsync(nameof(NotifyActivity), It.IsAny<Notification>(), It.IsAny<WorkflowTaskOptions>()),
Times.Exactly(2));
}
}
19 changes: 0 additions & 19 deletions test/Dapr.Workflow.Test/DaprWorlflowClientTests.cs

This file was deleted.

Loading