Skip to content

Commit

Permalink
Enable unit testing for Dapr Workflows (dapr#1035)
Browse files Browse the repository at this point in the history
* Workflow SDK changes to enable unit testing

Signed-off-by: Chris Gillum <cgillum@microsoft.com>

* Sample workflow unit testing project

Signed-off-by: Chris Gillum <cgillum@microsoft.com>

---------

Signed-off-by: Chris Gillum <cgillum@microsoft.com>
  • Loading branch information
cgillum authored and yash-nisar committed Feb 27, 2023
1 parent dfd4aeb commit 924a05a
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 23 deletions.
5 changes: 5 additions & 0 deletions all.sln
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PublishEventExample", "exam
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BulkPublishEventExample", "examples\Client\PublishSubscribe\BulkPublishEventExample\BulkPublishEventExample.csproj", "{DDC41278-FB60-403A-B969-2AEBD7C2D83C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowUnitTest", "examples\Workflow\WorkflowUnitTest\WorkflowUnitTest.csproj", "{8CA09061-2BEF-4506-A763-07062D2BD6AC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -244,6 +246,8 @@ Global
{DDC41278-FB60-403A-B969-2AEBD7C2D83C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DDC41278-FB60-403A-B969-2AEBD7C2D83C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DDC41278-FB60-403A-B969-2AEBD7C2D83C}.Release|Any CPU.Build.0 = Release|Any CPU
{8CA09061-2BEF-4506-A763-07062D2BD6AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8CA09061-2BEF-4506-A763-07062D2BD6AC}.Release|Any CPU.ActiveCfg = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -288,6 +292,7 @@ Global
{0EF6EA64-D7C3-420D-9890-EAE8D54A57E6} = {A7F41094-8648-446B-AECD-DCC2CC871F73}
{4A175C27-EAFE-47E7-90F6-873B37863656} = {0EF6EA64-D7C3-420D-9890-EAE8D54A57E6}
{DDC41278-FB60-403A-B969-2AEBD7C2D83C} = {0EF6EA64-D7C3-420D-9890-EAE8D54A57E6}
{8CA09061-2BEF-4506-A763-07062D2BD6AC} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40}
Expand Down
11 changes: 8 additions & 3 deletions examples/Workflow/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,16 @@ This Dapr workflow example shows how to create a Dapr workflow (`Workflow`) and

## Projects in sample

This sample contains a single [WorkflowConsoleApp](./WorkflowConsoleApp) .NET project. It utilizes the workflow SDK as well as the workflow management API for starting and querying workflows instances.
This sample contains a single [WorkflowConsoleApp](./WorkflowConsoleApp) .NET project.
It utilizes the workflow SDK as well as the workflow management API for starting and querying workflows instances.
The main `Program.cs` file contains the main setup of the app, including the registration of the workflow and workflow activities.
The workflow definition is found in the `Workflows` directory and the workflow activity definitions are found in the `Activities` directory.

The main `Program.cs` file contains the main setup of the app, including the registration of the workflow and workflow activities. The workflow definition is found in the `Workflows` directory and the workflow activity definitions are found in the `Activities` directory.
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.

## Running the example
## 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:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
using System.Threading.Tasks;
using Dapr.Workflow;
using Dapr.Workflow;
using Microsoft.Extensions.Logging;

namespace WorkflowConsoleApp.Activities
{
record Notification(string Message);
public record Notification(string Message);

class NotifyActivity : WorkflowActivity<Notification, object>
public class NotifyActivity : WorkflowActivity<Notification, object>
{
readonly ILogger logger;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
using System.Threading.Tasks;
using Dapr.Client;
using Dapr.Client;
using Dapr.Workflow;
using Microsoft.Extensions.Logging;
using WorkflowConsoleApp.Models;

namespace WorkflowConsoleApp.Activities
{
class ProcessPaymentActivity : WorkflowActivity<PaymentRequest, object>
public class ProcessPaymentActivity : WorkflowActivity<PaymentRequest, object>
{
readonly ILogger logger;
readonly DaprClient client;

public ProcessPaymentActivity(ILoggerFactory loggerFactory, DaprClient client)
public ProcessPaymentActivity(ILoggerFactory loggerFactory)
{
this.logger = loggerFactory.CreateLogger<ProcessPaymentActivity>();
this.client = client;
}

public override async Task<object> RunAsync(WorkflowActivityContext context, PaymentRequest req)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace WorkflowConsoleApp.Activities
{
class ReserveInventoryActivity : WorkflowActivity<InventoryRequest, InventoryResult>
public class ReserveInventoryActivity : WorkflowActivity<InventoryRequest, InventoryResult>
{
readonly ILogger logger;
readonly DaprClient client;
Expand Down
12 changes: 6 additions & 6 deletions examples/Workflow/WorkflowConsoleApp/Models.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
namespace WorkflowConsoleApp.Models
{
record OrderPayload(string Name, double TotalCost, int Quantity = 1);
record InventoryRequest(string RequestId, string ItemName, int Quantity);
record InventoryResult(bool Success, InventoryItem orderPayload);
record PaymentRequest(string RequestId, string ItemName, int Amount, double Currency);
record OrderResult(bool Processed);
record InventoryItem(string Name, double PerItemCost, int Quantity);
public record OrderPayload(string Name, double TotalCost, int Quantity = 1);
public record InventoryRequest(string RequestId, string ItemName, int Quantity);
public record InventoryResult(bool Success, InventoryItem orderPayload);
public record PaymentRequest(string RequestId, string ItemName, int Amount, double Currency);
public record OrderResult(bool Processed);
public record InventoryItem(string Name, double PerItemCost, int Quantity);
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
using System.Threading.Tasks;
using Dapr.Workflow;
using Dapr.Workflow;
using DurableTask.Core.Exceptions;
using WorkflowConsoleApp.Activities;
using WorkflowConsoleApp.Models;

namespace WorkflowConsoleApp.Workflows
{
class OrderProcessingWorkflow : Workflow<OrderPayload, OrderResult>
public class OrderProcessingWorkflow : Workflow<OrderPayload, OrderResult>
{
public override async Task<OrderResult> RunAsync(WorkflowContext context, OrderPayload order)
{
Expand Down
86 changes: 86 additions & 0 deletions examples/Workflow/WorkflowUnitTest/OrderProcessingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System.Threading.Tasks;
using Dapr.Workflow;
using Microsoft.DurableTask;
using Moq;
using WorkflowConsoleApp.Activities;
using WorkflowConsoleApp.Models;
using WorkflowConsoleApp.Workflows;
using Xunit;

namespace WorkflowUnitTest
{
[Trait("Example", "true")]
public class OrderProcessingTests
{
[Fact]
public async Task TestSuccessfulOrder()
{
// 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, new InventoryItem(order.Name, 9.99, order.Quantity));

// Mock the call to ReserveInventoryActivity
Mock<WorkflowContext> mockContext = new();
mockContext
.Setup(ctx => ctx.CallActivityAsync<InventoryResult>(nameof(ReserveInventoryActivity), It.IsAny<InventoryRequest>(), It.IsAny<TaskOptions>()))
.Returns(Task.FromResult(inventoryResult));

// 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<TaskOptions>()),
Times.Once());

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

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

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

// Mock the call to ReserveInventoryActivity
Mock<WorkflowContext> mockContext = new();
mockContext
.Setup(ctx => ctx.CallActivityAsync<InventoryResult>(nameof(ReserveInventoryActivity), It.IsAny<InventoryRequest>(), It.IsAny<TaskOptions>()))
.Returns(Task.FromResult(inventoryResult));

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

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

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

// Verify that there were two calls to NotifyActivity
mockContext.Verify(
ctx => ctx.CallActivityAsync(nameof(NotifyActivity), It.IsAny<Notification>(), It.IsAny<TaskOptions>()),
Times.Exactly(2));
}
}
}
28 changes: 28 additions & 0 deletions examples/Workflow/WorkflowUnitTest/WorkflowUnitTest.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>10</LangVersion>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\WorkflowConsoleApp\WorkflowConsoleApp.csproj" />
</ItemGroup>

</Project>

0 comments on commit 924a05a

Please sign in to comment.