Currently there is not a way to unit test Power Automate flows. You have the ability to manually run a flow with static results, but this isn't the same as a unit test. I have during my work implemented business critical functionality in Power Automate using Common Data Service (current environment) connector.
There is a way to unit test Power Automate flows!
A the NuGet package to your project.
This is a skeleton and itself will not test anything OOB. Instead this is meant as the core, used to implement different connectors. As you probably know, Power Automate uses Connectors to interact with others services, such as Common Data Service. I have implemented Common Data Service (current environment), go take a look to see how it can be used.
var path = "<path to flow definition>";
// from Microsoft.Extensions.DependencyInjection
var services = new ServiceCollection();
// Required to set up required dependencies
services.AddFlowRunner();
var sp = services.BuildServiceProvider();
var flowRunner = sp.GetRequiredService<FlowRunner>();
flowRunner.InitializeFlowRunner(path);
var flowResult = await flowRunner.Trigger();
// Your flow have now ran
This is optional and the settings class has the default values mentioned below.
The settings object is configured this way:
services.Configure<FlowSettings>(x => { }); // Optional way to add settings
The possible values to set is:
x.FailOnUnknownAction
(default: true
): If an action cannot be found and exception is thrown. This can be avoid and the action is ignored and the status is assumed to be Succeeded
.x.IgnoreActions
(default: empty
): List of action names which are ignored during execution, the action is not executed and the status is assumed to be Succeeded
.
x.LogActionsStates
(default: true
): Logs JSON, parsed input and generated output for every action executed.The FlowReport from triggering the flow can be used to assert the input and output of an action.
This can be used to verify that the expected parameters to an action is present and you can assert the input is as expected.
var greetingCardItems = flowReport.ActionStates["Create_a_new_row_-_Create_greeting_note"]
.ActionInput?["parameters"]?["item"];
Assert.IsNotNull(greetingCardItems);
Assert.AreEqual(expectedNoteSubject, greetingCardItems["subject"]);
Assert.AreEqual(expectedNoteText, greetingCardItems["notetext"]);
Actions can be added in three ways
services.AddFlowActionByName<GetMsnWeather>("Get_forecast_for_today_(Metric)");
When the action named Get_forecast_fortoday(Metric) is reached and about to be executed, the class with type GetMsnWeather is retrieved from the ServiceProvider and used to execute the action.
// For OpenApiConnection connectors only
services.AddFlowActionByApiIdAndOperationsName<Notification>(
"/providers/Microsoft.PowerApps/apis/shared_flowpush",
new []{ "SendEmailNotification", "SendNotification" });
When an action from the Notification connector with one of the supported types is reached in the flow, a action executor instance of type Notification
is created and used to execute the action.
services.AddFlowActionByFlowType<IfActionExecutor>("If");
When the generic action type If i reached, an action executor instance of type IfActionExecutor
is created and used to execute the action.
This is not recommended due to the fact that every OpenApiConnection connector will have the type OpenApiConnection. This means that both Common Data Service (current environment) and many others, will use the same action executors, which is not the correct way to do it.
This way of resolving an action executor is only used to resolve actions, where only one Action uses that type. This is If, DoUntil etc.
Currently there are two classes to extend, one is DefaultBaseActionExecutor and the other is OpenApiConnectionBaseActionExecutor.
private class ActionExecutor : DefaultBaseActionExecutor
{
private readonly IExpressionEngine _expression;
// Using dependency injection to get dependencies
public TriggerActionExecutor(IExpressionEngine expression)
{
_expression = expression ?? throw new ArgumentNullException(nameof(expression));
}
public override Task<ActionResult> Execute()
{
var result = new ActionResult();
try
{
// Some dangerous operation
// ...
result.ActionOutput = new ValueContainer(new Dictionary<string, ValueContainer>()
{
{"Key", new ValueContainer("Value")}
});
// Corresponds to: outputs('<action>').Key || outputs('<action>')['Key']
result.ActionStatus = ActionStatus.Succeeded;
}
catch(EvenMoreDangerousException exp)
{
// PAMU handles the exceptions...
result.ActionStatus = ActionStatus.Failed;
result.ActionExecutorException = exp;
}
return Task.FromResult(result);
}
}
The execute method is called when the action is run.
private class ActionExecutor : OpenApiConnectionActionExecutorBase
{
// To easier register the Action Executor
public const string FlowActionName = "Update_Account_-_Invalid_Id";
public override Task<ActionResult> Execute()
{
// ... Execute action functionality
var parameters = Parameters;
var entityName = parameters["string"].GetValue<string>();
// ...
}
}
When using OpenApiConnectionActionExecutorBase, some extra values form the Json definition is parsed and ready to use. These are currently values in the parameter object and host object.
Power Automate MockUp heavily depends on dependencies and its a great way to easily get different classes, in your own class. Below is a list of dependencies you might want to use, when executing flows.
ValueContainer value = expressionParser.Parse("<expression>");
An expression is known from Power Automate such as "@toLower('Jonh Doe')"
or "@outputs('Get_contacts')[2]"
.
Every expression from Power Automate is supported in PowerAutomateMockUp.
The response is returned wrap in the ValueContainer.
IState, and its implementation, is how the state of the execution of a Power Automate flow is handled. It contains the trigger values and can give the value of previous executed actions, by either using one of the interfaces below (for simplicity) or IState itself.
Tests are located in the Tests project and they are written using Nunit as test framework.
Feel free to create an issue with a suggestion, create a pull request or participate in any way you like :rocket:
The code is written using Riders default C# code style.
Commits are written in conventional commit style, the commit messages are used to determine the version and when to release a new version. The pipeline is hosted on Github and Semantic Release is used.
MIT