diff --git a/all.sln b/all.sln index 16df5b3d7..47fc9098c 100644 --- a/all.sln +++ b/all.sln @@ -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 @@ -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 @@ -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} diff --git a/examples/Workflow/README.md b/examples/Workflow/README.md index b1e7a1f93..610077d47 100644 --- a/examples/Workflow/README.md +++ b/examples/Workflow/README.md @@ -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: diff --git a/examples/Workflow/WorkflowConsoleApp/Activities/NotifyActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/NotifyActivity.cs index a6324947c..7c4616bd0 100644 --- a/examples/Workflow/WorkflowConsoleApp/Activities/NotifyActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/NotifyActivity.cs @@ -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 + public class NotifyActivity : WorkflowActivity { readonly ILogger logger; diff --git a/examples/Workflow/WorkflowConsoleApp/Activities/ProcessPaymentActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/ProcessPaymentActivity.cs index 8132e7bee..dc4cc531b 100644 --- a/examples/Workflow/WorkflowConsoleApp/Activities/ProcessPaymentActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/ProcessPaymentActivity.cs @@ -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 + public class ProcessPaymentActivity : WorkflowActivity { readonly ILogger logger; - readonly DaprClient client; - public ProcessPaymentActivity(ILoggerFactory loggerFactory, DaprClient client) + public ProcessPaymentActivity(ILoggerFactory loggerFactory) { this.logger = loggerFactory.CreateLogger(); - this.client = client; } public override async Task RunAsync(WorkflowActivityContext context, PaymentRequest req) diff --git a/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs index 48abca09e..7dce1f46a 100644 --- a/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs @@ -6,7 +6,7 @@ namespace WorkflowConsoleApp.Activities { - class ReserveInventoryActivity : WorkflowActivity + public class ReserveInventoryActivity : WorkflowActivity { readonly ILogger logger; readonly DaprClient client; diff --git a/examples/Workflow/WorkflowConsoleApp/Models.cs b/examples/Workflow/WorkflowConsoleApp/Models.cs index 9f3720cb0..f2f3dcbc8 100644 --- a/examples/Workflow/WorkflowConsoleApp/Models.cs +++ b/examples/Workflow/WorkflowConsoleApp/Models.cs @@ -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); } diff --git a/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs b/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs index b9b199a91..90b85a83b 100644 --- a/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs +++ b/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs @@ -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 + public class OrderProcessingWorkflow : Workflow { public override async Task RunAsync(WorkflowContext context, OrderPayload order) { diff --git a/examples/Workflow/WorkflowUnitTest/OrderProcessingTests.cs b/examples/Workflow/WorkflowUnitTest/OrderProcessingTests.cs new file mode 100644 index 000000000..0cb450fcd --- /dev/null +++ b/examples/Workflow/WorkflowUnitTest/OrderProcessingTests.cs @@ -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(), order.Name, order.Quantity, order.TotalCost); + InventoryRequest expectedInventoryRequest = new(It.IsAny(), order.Name, order.Quantity); + InventoryResult inventoryResult = new(Success: true, new InventoryItem(order.Name, 9.99, order.Quantity)); + + // Mock the call to ReserveInventoryActivity + Mock mockContext = new(); + mockContext + .Setup(ctx => ctx.CallActivityAsync(nameof(ReserveInventoryActivity), It.IsAny(), It.IsAny())) + .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(nameof(ReserveInventoryActivity), expectedInventoryRequest, It.IsAny()), + Times.Once()); + + // Verify that ProcessPaymentActivity was called with a specific input + mockContext.Verify( + ctx => ctx.CallActivityAsync(nameof(ProcessPaymentActivity), expectedPaymentRequest, It.IsAny()), + Times.Once()); + + // Verify that there were two calls to NotifyActivity + mockContext.Verify( + ctx => ctx.CallActivityAsync(nameof(NotifyActivity), It.IsAny(), It.IsAny()), + 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(), order.Name, order.Quantity); + InventoryResult inventoryResult = new(Success: false, null); + + // Mock the call to ReserveInventoryActivity + Mock mockContext = new(); + mockContext + .Setup(ctx => ctx.CallActivityAsync(nameof(ReserveInventoryActivity), It.IsAny(), It.IsAny())) + .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(nameof(ReserveInventoryActivity), expectedInventoryRequest, It.IsAny()), + Times.Once()); + + // Verify that ProcessPaymentActivity was never called + mockContext.Verify( + ctx => ctx.CallActivityAsync(nameof(ProcessPaymentActivity), It.IsAny(), It.IsAny()), + Times.Never()); + + // Verify that there were two calls to NotifyActivity + mockContext.Verify( + ctx => ctx.CallActivityAsync(nameof(NotifyActivity), It.IsAny(), It.IsAny()), + Times.Exactly(2)); + } + } +} diff --git a/examples/Workflow/WorkflowUnitTest/WorkflowUnitTest.csproj b/examples/Workflow/WorkflowUnitTest/WorkflowUnitTest.csproj new file mode 100644 index 000000000..9c4a74a17 --- /dev/null +++ b/examples/Workflow/WorkflowUnitTest/WorkflowUnitTest.csproj @@ -0,0 +1,28 @@ + + + + net6.0 + enable + 10 + false + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + +