Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable unit testing for Dapr Workflows #1035

Merged
merged 2 commits into from
Feb 16, 2023
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
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>
98 changes: 98 additions & 0 deletions src/Dapr.Workflow/DaprWorkflowContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// ------------------------------------------------------------------------
// 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.
// ------------------------------------------------------------------------

namespace Dapr.Workflow
{
using System;
using Microsoft.DurableTask;
using System.Threading.Tasks;
using System.Threading;

class DaprWorkflowContext : WorkflowContext
{
readonly TaskOrchestrationContext innerContext;

internal DaprWorkflowContext(TaskOrchestrationContext innerContext)
{
this.innerContext = innerContext ?? throw new ArgumentNullException(nameof(innerContext));
}

public override string Name => this.innerContext.Name;

public override string InstanceId => this.innerContext.InstanceId;

public override DateTime CurrentUtcDateTime => this.innerContext.CurrentUtcDateTime;

public override bool IsReplaying => this.innerContext.IsReplaying;

public override Task CallActivityAsync(string name, object? input = null, TaskOptions? options = null)
{
return this.innerContext.CallActivityAsync(name, input, options);
}

public override Task<T> CallActivityAsync<T>(string name, object? input = null, TaskOptions? options = null)
{
return this.innerContext.CallActivityAsync<T>(name, input, options);
}

public override Task CreateTimer(TimeSpan delay, CancellationToken cancellationToken = default)
{
return this.innerContext.CreateTimer(delay, cancellationToken);
}

public override Task CreateTimer(DateTime fireAt, CancellationToken cancellationToken)
{
return this.innerContext.CreateTimer(fireAt, cancellationToken);
}

public override Task<T> WaitForExternalEventAsync<T>(string eventName, CancellationToken cancellationToken = default)
{
return this.innerContext.WaitForExternalEvent<T>(eventName, cancellationToken);
}

public override Task<T> WaitForExternalEventAsync<T>(string eventName, TimeSpan timeout)
{
return this.innerContext.WaitForExternalEvent<T>(eventName, timeout);
}

public override void SendEvent(string instanceId, string eventName, object payload)
{
this.innerContext.SendEvent(instanceId, eventName, payload);
}

public override void SetCustomStatus(object? customStatus)
{
this.innerContext.SetCustomStatus(customStatus);
}

public override Task<TResult> CallChildWorkflowAsync<TResult>(string workflowName, object? input = null, TaskOptions? options = null)
{
return this.innerContext.CallSubOrchestratorAsync<TResult>(workflowName, input, options);
}

public override Task CallChildWorkflowAsync(string workflowName, object? input = null, TaskOptions? options = null)
{
return this.innerContext.CallSubOrchestratorAsync(workflowName, input, options);
}

public override void ContinueAsNew(object? newInput = null, bool preserveUnprocessedEvents = true)
{
this.innerContext.ContinueAsNew(newInput!, preserveUnprocessedEvents);
}

public override Guid NewGuid()
{
return this.innerContext.NewGuid();
}
}
}
Loading