Microservices with Dapr #9: Testing Workflows
In the post #7 of this series, we've added a CurrencyExchange Workflow. Now let's explore different ways of testing it.
Adding Unit Tests for revised CurrencyExchange Workflow
First of all, I introduced some changes in the implementation of CurrencyExchangeWorkflow. Workflow’s RunAsync() method is now much shorter:
That’s because the main part of its logic is moved to RunExchangeTransfer() and what’s left is handling of confirmation step. The remaining “transfer” part looks as follows:
After playing a bit with this sequence of steps I decided to move some error handling / retry policy related code to Debit-/Credit- AccountActivity and add so called “compensation step” to the workflow. The current approach is that if Activity result says “Succeeded”, Workflow is going to wait for a corresponding event that confirms command completion. But if for some reason Credit command failed, we want to restore the initial state and rollback initial Debit command. In such case CurrencyExchange workflow can reach its final “unsuccessful” state regardless of the reasons that interrupted the process. I basically tried eliminating the risks of workflow ending in an inconsistent state. The other important factor to consider could be controlling the overall execution time of entire currency exchange transaction which could be achieved by specifying timeouts for each “wait …” call
but even then we would still want to include some “compensation steps”.
Regardless of implementation details, the main dependency for Workflow is WorkflowContext. The “happy path” test that creates mock/substitute using NSubstitute library could look like this:
With all the relevant WorkflowContext methods having mock overrides, unit-testing our Dapr Workflows is not different from writing tests for any class. As another example, we can check whether the compensation step is properly invoked by setting call to CreditAccountActivity to always return failed result for beneficiary account:
Running end-to-end tests against live environment
Unit tests coverage is important as initial verification of the code but it doesn’t tell us anything about how the same code will behave when it’s built into executable, deployed and run on the actual environment. With Dapr Workflow as an example, we could only speculate if all the magic of “behind-the-scenes” Event Sourcing, interactions between Sidecars and State Store etc. runs exactly as intended…
Let’s move to a less boring example. First of all, we need to deploy latest versions of all 3 services to Azure Container Apps environment. Here is our test environment after successful deployment:
To run automated end-to-end test we are going to use VS Code with a really cool Extension I’ve recently discovered: httpYac
We want to prepare a script that would orchestrate and execute a sequence of related HTTP requests to SavingsOnDapr API endpoints. We want this test to be repeatable and independent of state already persisted in the DB so it would be preferable to first create accounts that we want to interact with. The test plan could be described in following steps:
Create CurrentAccount for USD currency.
Verify that account exists.
Credit account created in step 1. with initial amount.
Create CurrentAccount for EUR currency.
Verify that account exits.
Query for Currency Exchange Rate for ‘USD => EUR’ and save the value.
Send CurrencyExchange Order from USD account to EUR with “LimitOrder” type and Exchange Rate higher than current one.
Check that CurrencyExchange Order is not immediately fulfilled.
Call endpoint to modify ExchangeRate for ‘USD => EUR’ with value that would satisfy the order from step 7.
Wait for ~60s (CurrencyExchange Workflow is set to check pending order every minute).
Check that CurrencyExchange Order is now fulfilled.
After running some experiments with httpYac and learning the syntax (there is quite helpful “guide / getting started” on httpYac project page), I’ve managed to create a .http file that covers the test plan presented above.
###
# @name createUsdAccount
{{
exports.usdAccRef = "testacc-usd-{{$guid}}"
}}
POST {{hostSavings}}/api/accounts
Content-Type: application/json
Accept: application/json
{
"externalRef" : "{{usdAccRef}}",
"accountCurrency" : "USD"
}
{{
exports.usdAccHeader = response.headers['location'];
}}
###
# @ref createUsdAccount
# @name checkUsdAccount
GET {{hostSavings}}{{usdAccHeader}}
?? status == 200
###
# @name creditUsdAccount
# @ref checkUsdAccount
POST {{hostSavings}}/api/accounts/:credit
Content-Type: application/json
Accept: application/json
{
"externalRef" : "{{checkUsdAccount[0].externalRef}}",
"amount" : 12001.00,
"transactionDate" : "2024-11-20T16:55:08.808Z"
}
###
# @name createEurAccount
{{
exports.eurRef = "testacc-eur-{{$guid}}"
}}
POST {{hostSavings}}/api/accounts
Content-Type: application/json
Accept: application/json
{
"externalRef" : "{{eurRef}}",
"accountCurrency" : "EUR"
}
{{
exports.eurAccHeader = response.headers['location'];
}}
?? status == 202
###
# @ref createEurAccount
# @name checkEurAccount
GET {{hostSavings}}{{eurAccHeader}}
?? status == 200
###
# @name exchquery
POST {{hostExchange}}/v1/currency-exchange-query
Content-Type: application/json
Accept: application/json
{
"source" : "USD",
"target" : "EUR",
"amount" : 6000
}
?? status == 200
?? body contains rate
?? js response.parsedBody.exchangeType == USD => EUR
###
# @ref exchquery
# @ref checkUsdAccount
# @ref checkEurAccount
# @name exchorder
{{
exports.requestedRate = exchquery.rate + 0.2;
}}
POST {{hostExchange}}/v1/currency-exchange-order
Content-Type: application/json
Accept: application/json
{
"orderId": "limitorder-{{$guid}}",
"debtorExternalRef": "{{checkUsdAccount[0].externalRef}}",
"beneficiaryExternalRef": "{{checkEurAccount[0].externalRef}}",
"sourceCurrency": "USD",
"targetCurrency": "EUR",
"sourceAmount": 6000.50,
"exchangeRate": {{requestedRate}},
"orderType": "LimitOrder"
}
{{
exports.exchorderloc = response.headers['location'];
}}
?? status == 202
?? header location exists
###
# @ref exchorder
# @name exchorderstatus1
# @sleep 100
GET {{hostExchange}}/{{exchorderloc}}
Accept: application/json
?? status == 202
###
# @name exchRateModify
# @ref exchorder
{{
exports.modRate = requestedRate + 0.15;
}}
POST {{hostExchange}}/v1/currency-exchange-rate
Content-Type: application/json
Accept: application/json
{
"source": "USD",
"target": "EUR",
"baseRate": {{modRate}}
}
###
# @ref exchorder
# @name exchorderstatus2
# @sleep 75_000
GET {{hostExchange}}/{{exchorderloc}}
Accept: application/json
?? status == 200
?? js response.parsedBody.status == CompletedThat’s how it looks when you run the script from “Testing” panel:
After the tests are completed, you can navigate to each step and click “Test Results” link that opens a separate tab with the log from execution:
I’ve found this VS Code Extension really useful. I must say I’m amazed by how well it’s integrated into IDE. It’s also nice to see that Dapr Workflow building-block works seamlessly when being run on Azure Container App with built-in Dapr support!
That’s all for now. Thanks for reading!








