TL;DR
In Part 4 , we learned LogicAppUnit fundamentals. Now we build real test suites using production workflows that mix HTTP actions and ServiceProvider connectors. You'll see project structure, mock data management, happy/sad path testing, and debugging techniques - all from working production code.
What You'll Learn
β
Structure testable Logic App projects
β
Mock ServiceProvider connectors (Blob, SAS URLs)
β
Test happy paths and error handling
β
Manage mock data with JSON files
β
Debug test failures efficiently
The Workflows
π· Product Creation (Complex)
Flow: Validate β Create Product β Upload Blob β Get SAS URL β Update Metadata β Respond
Tests: Error handling, validation, request inspection
π· Blob Search (Simple)
Flow: Build Request β Call Function β Check Result β Respond
Tests: Success, empty results, API errors
Project Structure
AzMCP/
βββ src/
β βββ LABasicDemo/ # Logic App
β β βββ wf-product-create/ # HTTP action workflow
β β β βββ workflow.json
β β βββ wf-builtin-connector/ # Built-in connector (Azure Blob)
β β β βββ workflow.json
β β βββ wf-mngconnector-pub/ # Managed connector(Service Bus, O365)
β β β βββ workflow.json
β β βββ wf-mngconnector-sub/
β β β βββ workflow.json
β β βββ parameters.json # @appsetting() refs
β β βββ connections.json # Production connections
β β βββ local.settings.json
β β
β βββ CommonComps.UnitTests/ # Tests
β βββ LogicApp/
β β βββ wf-product-create/
β β β βββ WfProductCreateTests.cs
β β β βββ MockData/
β β β βββ CreateProductResponse.json
β β β βββ UploadBlobResponse.json
β β βββ wf-builtin-connector/
β β β βββ WfBuiltInConnectorTests.cs
β β β βββ MockData/
β β β βββ UploadBlobResponse.json
β β β βββ GetBlobSASUriResponse.json
β β βββ wf-mngconnector-pub/
β β β βββ WfMngConnectorPubTests.cs
β β β βββ MockData/
β β β βββ Office365SendEmailResponse.json
β β β βββ ServiceBusSendMessageResponse.json
β β βββ wf-mngconnector-sub/
β β β βββ WfMngConnectorSubTests.cs
β β β βββ MockData/
β β β βββ ServiceBusTriggerPayload.json
β β βββ Helpers/
β β βββ TestConfigHelper.cs
β βββ testConfiguration.json
β βββ parameters.unittest.json
β βββ connection.unittest.json
β βββ local.setting.unittest.json
Configuration Files
parameters.json (Production)
{
"apiUrl": {
"value": "@appsetting('apiUrl')"
},
"servicebus-ConnectionRuntimeUrl": {
"value": "@appsetting('servicebus-ConnectionRuntimeUrl')"
},
"servicebus-Authentication": {
"type": "Object",
"value": {
"type": "ManagedServiceIdentity"
}
},
"office365-ConnectionRuntimeUrl": {
"value": "@appsetting('office365-ConnectionRuntimeUrl')"
},
"office365-Authentication": {
"type": "Object",
"value": {
"type": "ManagedServiceIdentity"
}
}
}
parameters.unittest.json (Test Override)
{
"apiUrl": {
"value": "@appsetting('apiUrl')"
},
"servicebus-ConnectionRuntimeUrl": {
"value": "@appsetting('servicebus-ConnectionRuntimeUrl')"
},
"servicebus-Authentication": {
"type": "Object",
"value": {
"type": "Raw",
"scheme": "Key",
"parameter": "@appsetting('servicebus-connectionKey')"
}
},
"office365-ConnectionRuntimeUrl": {
"value": "@appsetting('office365-ConnectionRuntimeUrl')"
},
"office365-Authentication": {
"type": "Object",
"value": {
"type": "Raw",
"scheme": "Key",
"parameter": "@appsetting('office365-connectionKey')"
}
}
}
local.settings.json
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "node",
"AzureWebJobsFeatureFlags": "EnableWorkerIndexing",
"apiUrl": "https://your-api.azurewebsites.net/api",
"AzureBlob_connectionString": "DefaultEndpointsProtocol=https;AccountName=youraccount;AccountKey=dummy;EndpointSuffix=core.windows.net",
"servicebus-connectionKey": "dummy-key-for-tests",
"servicebus-ConnectionRuntimeUrl": "https://dummy-servicebus.servicebus.windows.net",
"office365-connectionKey": "dummy-key-for-tests",
"office365-ConnectionRuntimeUrl": "https://dummy-office365.office365.com",
"WORKFLOWS_SUBSCRIPTION_ID": "00000000-0000-0000-0000-000000000000",
"WORKFLOWS_LOCATION_NAME": "eastus",
"WORKFLOWS_RESOURCE_GROUP_NAME": "rg-test"
}
}
connections.json (Production)
{
"serviceProviderConnections": {
"AzureBlob": {
"parameterValues": {
"connectionString": "@appsetting('AzureBlob_connectionString')"
},
"parameterSetName": "connectionString",
"serviceProvider": {
"id": "/serviceProviders/AzureBlob"
}
}
},
"managedApiConnections": {
"servicebus": {
"api": {
"id": "/subscriptions/@{appsetting('WORKFLOWS_SUBSCRIPTION_ID')}/providers/Microsoft.Web/locations/@{appsetting('WORKFLOWS_LOCATION_NAME')}/managedApis/servicebus"
},
"connection": {
"id": "/subscriptions/@{appsetting('WORKFLOWS_SUBSCRIPTION_ID')}/resourceGroups/@{appsetting('WORKFLOWS_RESOURCE_GROUP_NAME')}/providers/Microsoft.Web/connections/servicebus"
},
"connectionRuntimeUrl": "@parameters('servicebus-ConnectionRuntimeUrl')",
"authentication": "@parameters('servicebus-Authentication')"
},
"office365": {
"api": {
"id": "/subscriptions/@{appsetting('WORKFLOWS_SUBSCRIPTION_ID')}/providers/Microsoft.Web/locations/@{appsetting('WORKFLOWS_LOCATION_NAME')}/managedApis/office365"
},
"connection": {
"id": "/subscriptions/@{appsetting('WORKFLOWS_SUBSCRIPTION_ID')}/resourceGroups/@{appsetting('WORKFLOWS_RESOURCE_GROUP_NAME')}/providers/Microsoft.Web/connections/office365"
},
"connectionRuntimeUrl": "@parameters('office365-ConnectionRuntimeUrl')",
"authentication": "@parameters('office365-Authentication')"
}
}
}
connections.unittest.json (Test Override)
{
"serviceProviderConnections": {
"AzureBlob": {
"parameterValues": {
"connectionString": "@appsetting('AzureBlob_connectionString')"
},
"parameterSetName": "connectionString",
"serviceProvider": {
"id": "/serviceProviders/AzureBlob"
}
}
},
"managedApiConnections": {
"servicebus": {
"api": {
"id": "/subscriptions/@{appsetting('WORKFLOWS_SUBSCRIPTION_ID')}/providers/Microsoft.Web/locations/@{appsetting('WORKFLOWS_LOCATION_NAME')}/managedApis/servicebus"
},
"connection": {
"id": "/subscriptions/@{appsetting('WORKFLOWS_SUBSCRIPTION_ID')}/resourceGroups/@{appsetting('WORKFLOWS_RESOURCE_GROUP_NAME')}/providers/Microsoft.Web/connections/servicebus"
},
"authentication": {
"type": "Raw",
"scheme": "Key",
"parameter": "@appsetting('servicebus-connectionKey')"
},
"connectionRuntimeUrl": "@parameters('servicebus-ConnectionRuntimeUrl')"
},
"office365": {
"api": {
"id": "/subscriptions/@{appsetting('WORKFLOWS_SUBSCRIPTION_ID')}/providers/Microsoft.Web/locations/@{appsetting('WORKFLOWS_LOCATION_NAME')}/managedApis/office365"
},
"connection": {
"id": "/subscriptions/@{appsetting('WORKFLOWS_SUBSCRIPTION_ID')}/resourceGroups/@{appsetting('WORKFLOWS_RESOURCE_GROUP_NAME')}/providers/Microsoft.Web/connections/office365"
},
"authentication": {
"type": "Raw",
"scheme": "Key",
"parameter": "@appsetting('office365-connectionKey')"
},
"connectionRuntimeUrl": "@parameters('office365-ConnectionRuntimeUrl')"
}
}
}
testConfiguration.json
{
"azurite": {
"enableAzuritePortCheck": true
},
"logging": {
"writeMockRequestMatchingLogs": true
},
"workflow": {
"externalApiUrlsToMock": [
"https://your-api.azurewebsites.net"
],
"builtInConnectorsToMock": [
"uploadBlob",
"getBlobSASUri"
]
}
}
Key Configuration Points
Production vs Test:
-
parameters.jsonusesManagedServiceIdentityβparameters.unittest.jsonusesRawauth -
connections.jsonreferences@parameters()βconnections.unittest.jsonhas explicitRawauth - Test files are swapped in by
TestConfigHelperbeforeInitialize()
Critical Settings:
- All parameters use
@appsetting()- never hardcode values -
externalApiUrlsToMockcontains base URLs only (no paths) -
builtInConnectorsToMocklists operationIds from workflows - Managed connector auth MUST be
Rawtype for tests
Test Base Class
public class WfProductCreateTests : WorkflowTestBase, IDisposable
{
private readonly LogicAppTestConfigHelper _configHelper;
private static readonly string MockDataPath = Path.Combine(
AppDomain.CurrentDomain.BaseDirectory,
"../../../LogicApp/wf-product-create/MockData");
public WfProductCreateTests()
{
_configHelper = new LogicAppTestConfigHelper();
_configHelper.ApplyTestConfig("../LABasicDemo");
Initialize("../../../../LABasicDemo", "wf-product-create");
}
public void Dispose() => _configHelper?.RestoreAll();
private static JObject LoadMockData(string fileName) =>
JObject.Parse(File.ReadAllText(Path.Combine(MockDataPath, fileName)));
}
Example 1: Happy Path Test
[Fact]
public void CreateProduct_HappyPath_Success()
{
using (ITestRunner testRunner = CreateTestRunner())
{
// π Load mocks
var productResp = LoadMockData("CreateProductResponse.json");
var blobResp = LoadMockData("UploadBlobResponse.json");
var sasResp = LoadMockData("GetSasUrlResponse.json");
// π HTTP action - CreateProduct (INTERCEPTED)
testRunner.AddMockResponse(
MockRequestMatcher.Create()
.UsingPost()
.WithPath(PathMatchType.Contains, "/Products"))
.RespondWith(MockResponseBuilder.Create()
.WithSuccess()
.WithContentAsJson(productResp));
// π¦ Built-in - uploadblob (CONVERTED)
testRunner.AddMockResponse(
MockRequestMatcher.Create()
.UsingPost()
.WithPath(PathMatchType.Exact, "/uploadblob")
.FromAction("uploadblob"))
.RespondWith(MockResponseBuilder.Create()
.WithSuccess()
.WithContentAsJson(blobResp));
// π Built-in - GetSasUrl (CONVERTED)
testRunner.AddMockResponse(
MockRequestMatcher.Create()
.UsingPost()
.WithPath(PathMatchType.Exact, "/GetSasUrl")
.FromAction("GetSasUrl"))
.RespondWith(MockResponseBuilder.Create()
.WithSuccess()
.WithContentAsJson(sasResp));
// π·οΈ HTTP action - updatemetadata (INTERCEPTED)
testRunner.AddMockResponse(
MockRequestMatcher.Create()
.UsingPut()
.WithPath(PathMatchType.Contains, "uploadproductinfo"))
.RespondWith(MockResponseBuilder.Create().WithSuccess());
// π Trigger
var response = testRunner.TriggerWorkflow(
new StringContent(@"{
""name"": ""LaptopM4"",
""sku"": ""006"",
""categoryId"": 1,
""price"": 1500
}", Encoding.UTF8, "application/json"),
HttpMethod.Post);
// β
Assert
Assert.Equal(WorkflowRunStatus.Succeeded, testRunner.WorkflowRunStatus);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(ActionStatus.Succeeded,
testRunner.GetWorkflowActionStatus("uploadblob"));
}
}
Mock Data Files
CreateProductResponse.json (HTTP)
{
"id": 123,
"name": "LaptopM4",
"sku": "006",
"price": 1500
}
UploadBlobResponse.json (ServiceProvider - Raw!)
{
"blobUri": "http://localhost:7075/uploadproductinfo/LaptopM4006.json"
}
Critical:
- β NO
statusCode/bodywrapper - β
Points to
localhost:7075
Example 2: Validation Error
[Fact]
public void CreateProduct_WithoutCategoryId_Returns400()
{
using (ITestRunner testRunner = CreateTestRunner())
{
// β No mocks - fails before API calls
var response = testRunner.TriggerWorkflow(
new StringContent(@"{ ""name"": ""Test"" }",
Encoding.UTF8, "application/json"),
HttpMethod.Post);
// β
Assert
Assert.True(testRunner.WorkflowWasTerminated);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
Assert.Equal(ActionStatus.Skipped,
testRunner.GetWorkflowActionStatus("CreateProduct"));
}
}
Example 3: API Error Handling
[Fact]
public void CreateProduct_WhenApiFails_Returns500()
{
using (ITestRunner testRunner = CreateTestRunner())
{
// β Mock API failure
testRunner.AddMockResponse(
MockRequestMatcher.Create()
.UsingPost()
.WithPath(PathMatchType.Contains, "/Products"))
.RespondWith(MockResponseBuilder.Create()
.WithInternalServerError());
var response = testRunner.TriggerWorkflow(
new StringContent(@"{
""name"": ""Test"",
""categoryId"": 1
}", Encoding.UTF8, "application/json"),
HttpMethod.Post);
// β
Assert error handling
Assert.True(testRunner.WorkflowWasTerminated);
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
Assert.Equal(ActionStatus.Failed,
testRunner.GetWorkflowActionStatus("TryProcessProduct"));
}
}
Example 4: Request Inspection
[Fact]
public void CreateProduct_SetsCorrectMetadata()
{
using (ITestRunner testRunner = CreateTestRunner())
{
// ... setup all mocks ...
testRunner.TriggerWorkflow(/* payload */);
// π Inspect actual request
var mockRequests = testRunner.MockRequests;
var metadataReq = mockRequests.FirstOrDefault(r =>
r.Method == HttpMethod.Put &&
r.RequestUri.ToString().Contains("uploadproductinfo"));
// β
Verify header
metadataReq.Should().NotBeNull();
metadataReq.Headers.Should().ContainKey("x-ms-meta-productdoc");
metadataReq.Headers["x-ms-meta-productdoc"].First()
.Should().Be("LaptopM4006.json");
}
}
Example 5: Simple HTTP Workflow
[Fact]
public void BlobSearch_ReturnsResults()
{
using (ITestRunner testRunner = CreateTestRunner())
{
var searchResp = LoadMockData("BlobSearchResponse.json");
testRunner.AddMockResponse(
MockRequestMatcher.Create()
.UsingPost()
.WithPath(PathMatchType.Contains, "/BlobMetadataSearch"))
.RespondWith(MockResponseBuilder.Create()
.WithSuccess()
.WithContentAsJson(searchResp));
var response = testRunner.TriggerWorkflow(
new StringContent(@"{
""container"": ""products"",
""metadataKey"": ""productdoc"",
""metadataValue"": ""LaptopM4""
}", Encoding.UTF8, "application/json"),
HttpMethod.Post);
Assert.Equal(WorkflowRunStatus.Succeeded, testRunner.WorkflowRunStatus);
var json = JObject.Parse(response.Content.ReadAsStringAsync().Result);
Assert.Equal("success", json["status"]?.ToString());
Assert.Equal(2, json["count"]?.Value<int>());
}
}
Testing Strategy Matrix
| Scenario | Test | Verifies |
|---|---|---|
| Happy Path | All actions succeed | Status, actions, response |
| Validation | Missing field | Terminate, 400, skipped actions |
| API Error | External fail | Error handling, 500 |
| Empty Results | No data | Success, count=0 |
| Request | Payload check | Headers, content correct |
Running Tests
# Start Azurite
npx azurite --silent --location ./.azurite
# Run tests
dotnet test --filter "FullyQualifiedName~LogicApp"
# Single test with logging
LOGICAPPUNIT_TESTLOGGER_VERBOSITY=Trace \
dotnet test --filter "FullyQualifiedName~LogicApps"
Expected Output:
Debugging Quick Reference
π Mock Requests Count = 0
grep "@appsetting" parameters.json
cat testConfiguration.json | grep "externalApiUrlsToMock"
π Built-In Connector Null
// β WRONG
{ "statusCode": "OK", "body": { "blobUri": "..." } }
// β
CORRECT
{ "blobUri": "http://localhost:7075/..." }
π Action Skipped
// Find which action failed first
Console.WriteLine($"CreateProduct: {testRunner.GetWorkflowActionStatus("CreateProduct")}");
Console.WriteLine($"uploadblob: {testRunner.GetWorkflowActionStatus("uploadblob")}");
Key Takeaways
Structure
β
One test class per workflow
β
MockData in JSON files
β
Helper for config management
Patterns
β
HTTP actions β endpoint path
β
Built-in connectors β action name + .FromAction()
β
Dynamic URLs β PathMatchType.Contains
Testing
β
Test happy AND sad paths
β
Verify workflow + action statuses
β
Inspect requests when needed
β
Keep mock data realistic
Debugging
β
Check MockRequests count first
β
Enable writeMockRequestMatchingLogs
β
Verify response formats
β
Use focused test filters

Top comments (0)