Background
Logic Apps are a great tool for your toolbox. They allow you to spin up small units in your workflow for rapid development. Unfortunately, testing them is not the easiest.
One option you do have is to use mocks. Mocks allow you to substitute the output of a trigger or action with your own static result. That sounds good, but mocks are only available for some triggers and actions. Also, you’d have to do it for all executions of the Logic App or create a separate one for testing entirely.
Knowing the pitfalls pointed out above left me poking around for a new testing strategy. Below is what I ended up with.
The App We’re Testing
Testing Setup
In this walk-through, I’ll be using Visual Studio 2019 Professional and SpecFlow for Visual Studio 2019 extension. SpecFlow isn’t an absolutely necessary requirement. However, it is a useful tool for creating reusable easy-to-understand test scenarios. Also, if you have less technical testers on your team it’s a great tool for them because they can easily plug in new values.
Nuget Packages:
- coverlet.collector
- Microsoft.Azure.Management.Logic
- Microsoft.Azure.Management.ResourceManager.Fluent
- Microsoft.NET.Test.Sdk
- MSTest.TestAdapter
- MSTest.TestFramework
- SpecFlow
- SpecFlow.MsTest
- SpecFlow.Tools.MsBuild.Generation
Testing Outline
- Trigger the Logic App
- Query Logic App run history
- Parse trigger/actions and statuses
- Assert expected vs actual results
Trigger the Logic App
In this example, we’ll walk through triggering a Logic App via an HTTP endpoint. Substitute this process with whatever method is required for you to trigger your Logic App.
Remember, this is SpecFlow, so it may look a little unusual if you’re unfamiliar. SpecFlow binds human readable syntax to application code.
Scenario: TimeForCoffee noon test Given I send a POST request to 'TimeForCoffee' with the body | currentTime | | 12:00 |
This is the Given SpecFlow step definition to trigger the POST endpoint of the app. ‘TimeForCoffee’ is an alias to my app’s endpoint and the table is converted to the POST body.
Given(@"I send a POST request to '(.*)' with the body")] public void GivenISendAPOSTRequestToWithTheBody(string uriKey, Table table) { var json = JsonConvert.SerializeObject(table.Rows.First()); var content = new StringContent(json, Encoding.UTF8, "application/json"); _ = _client.PostAsync(HttpTriggerUris[uriKey], content).Result; }
We convert the Table rows directly to JSON, then POST it to the app’s endpoint to trigger execution.
Query Logic App run history
When your Logic App executes, execution information is made immediately available after completion. This information can be retrieved using the REST API or Azure SDK. The execution history will allow you to see each trigger/action executed and the resulting status.
[BeforeTestRun] public static void Initialize() { var credentials = SdkContext.AzureCredentialsFactory .FromServicePrincipal(EnvVariables.AzureClientId, EnvVariables.AzureClientSecret, EnvVariables.AzureTenantId, AzureEnvironment.AzureGlobalCloud); _retriever = new LogicAppExecutionRetriever(new LogicManagementClient(credentials) { SubscriptionId = EnvVariables.SubscriptionId }, EnvVariables.ResourceGroup); }
Here, we are creating the credentials required for the LogicManagementClient. The LogicManagementClient is the package we’ll be using to query the Logic App execution history.
private WorkflowRun GetLatestWorkflowRun(string logicApp) { return _client .WorkflowRuns .ListWithHttpMessagesAsync(_resourceGroup, logicApp) .Result.Body.First(); } private IEnumerable<WorkflowRunAction> GetWorkflowRunActions(string logicApp, string workflowRunId) { return _client .WorkflowRunActions .ListWithHttpMessagesAsync(_resourceGroup, logicApp, workflowRunId) .Result.Body.OrderBy(x => x.StartTime); }
These are the calls we’ll be using to retrieve the WorkflowRun and WorkflowRunActions from our most recent execution. The _client here is the LogicAppManagementClient.
Parse trigger/actions and statuses
There is way more information provided by the execution history than we need. As a result, we’ll be digging into the object and pulling out the essentials into a new class that’ll be easier to work with.
public IList<LogicAppEvent> GetLatestExecutionEvents(string logicApp) { var events = new List<LogicAppEvent>(); var workflowRun = GetLatestWorkflowRun(logicApp); events.Add(new LogicAppEvent(workflowRun.Trigger.Name, workflowRun.Trigger.Status)); var workflowRunActions = GetWorkflowRunActions(logicApp, workflowRun.Name); events.AddRange(workflowRunActions.Select(action => new LogicAppEvent(action.Name, action.Status))); return events; }
There are two different major pieces to this:
One, is when we query for the latest workflow run (GetLatestWorkflowRun). The query returns the trigger information so we go ahead and parse that into a LogicAppEvent using workflow.Trigger.Name and workflow.Trigger.Status.
Two, is when we query for the workflow run actions (GetWorkflowRunActions). We iterate over the returned actions and pull the action.Name and action.Status into our list of LogicAppEvents.
Assert expected vs actual results
Doing the above allows you to trigger the Logic App using various inputs and assert that the specific steps (think if/else control flow) get executed as expected.
Scenario: TimeForCoffee noon test When I send a POST request to 'TimeForCoffee' with the body | currentTime | | 12:00 | And I wait '5' seconds for execution to complete Then I can verify the following logic app events for 'TimeForCoffee' | StepName | Status | | manual | Succeeded | | Initialize_CupsOfCoffeeToDrink | Succeeded | | Get_hour_from_currentTime | Succeeded | | Should_get_a_cup_of_coffee | Succeeded | | Determine_number_of_cups | Succeeded | | Number_of_cups_to_drink | Succeeded | | Set_CupsOfCoffeeToDrink | Succeeded | | Until | Succeeded | | Drink_cup_of_coffee | Succeeded | | Decrement_CupsOfCoffeeToDrink | Succeeded | | Too_late_for_coffee | Skipped |
This is the final SpecFlow feature test. As you can see, we have a Then binding that evaluates a table of expected logic app events. These expected steps were extracted from our ‘TimeForCoffee’ Logic App execution.
As you can see, we are able to evaluate each and every step that the Logic App performed. This provides us the insight we need to confirm that certain events executed as expected.
[Then(@"I can verify the following logic app events for '(.*)'")] public void ThenICanVerifyTheFollowingLogicAppEventsFor(string logicApp, IList<LogicAppEvent> expectedEvents) { var actualEvents = _retriever.GetLatestExecutionEvents(logicApp); for (var i = 0; i < expectedEvents.Count; i++) { var expectedEvent = expectedEvents.ElementAt(i); LogicAppEvent actualEvent = null; try { actualEvent = actualEvents.ElementAt(i); } catch { Assert.Fail($"Expected to find ({expectedEvent}) and there were no more!"); } Assert.AreEqual(expectedEvent.ToString(), actualEvent.ToString()); } }
Here is the code behind the Then step. We get the latest events, then iterate the List of expectedEvents. With each expectedEvent we see if there’s a matching actualEvent.
One thing that does happen behind the scenes is we convert the raw input table into a List of LogicAppEvents using a custom SpecFlow Step Argument Transformation.
Leave a Reply