DEV Community

Daniel Jonathan
Daniel Jonathan

Posted on

Testing Azure Logic Apps: Implementation Guide

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
Enter fullscreen mode Exit fullscreen mode

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"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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')"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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')"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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')"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

testConfiguration.json

{
  "azurite": {
    "enableAzuritePortCheck": true
  },
  "logging": {
    "writeMockRequestMatchingLogs": true
  },
  "workflow": {
    "externalApiUrlsToMock": [
      "https://your-api.azurewebsites.net"
    ],
    "builtInConnectorsToMock": [
      "uploadBlob",
      "getBlobSASUri"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Key Configuration Points

Production vs Test:

  • parameters.json uses ManagedServiceIdentity β†’ parameters.unittest.json uses Raw auth
  • connections.json references @parameters() β†’ connections.unittest.json has explicit Raw auth
  • Test files are swapped in by TestConfigHelper before Initialize()

Critical Settings:

  • All parameters use @appsetting() - never hardcode values
  • externalApiUrlsToMock contains base URLs only (no paths)
  • builtInConnectorsToMock lists operationIds from workflows
  • Managed connector auth MUST be Raw type 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)));
}
Enter fullscreen mode Exit fullscreen mode

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"));
    }
}
Enter fullscreen mode Exit fullscreen mode

Mock Data Files

CreateProductResponse.json (HTTP)

{
  "id": 123,
  "name": "LaptopM4",
  "sku": "006",
  "price": 1500
}
Enter fullscreen mode Exit fullscreen mode

UploadBlobResponse.json (ServiceProvider - Raw!)

{
  "blobUri": "http://localhost:7075/uploadproductinfo/LaptopM4006.json"
}
Enter fullscreen mode Exit fullscreen mode

Critical:

  • ❌ NO statusCode/body wrapper
  • βœ… 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"));
    }
}
Enter fullscreen mode Exit fullscreen mode

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"));
    }
}
Enter fullscreen mode Exit fullscreen mode

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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>());
    }
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Expected Output:


Debugging Quick Reference

πŸ› Mock Requests Count = 0

grep "@appsetting" parameters.json
cat testConfiguration.json | grep "externalApiUrlsToMock"
Enter fullscreen mode Exit fullscreen mode

πŸ› Built-In Connector Null

// ❌ WRONG
{ "statusCode": "OK", "body": { "blobUri": "..." } }

// βœ… CORRECT
{ "blobUri": "http://localhost:7075/..." }
Enter fullscreen mode Exit fullscreen mode

πŸ› Action Skipped

// Find which action failed first
Console.WriteLine($"CreateProduct: {testRunner.GetWorkflowActionStatus("CreateProduct")}");
Console.WriteLine($"uploadblob: {testRunner.GetWorkflowActionStatus("uploadblob")}");
Enter fullscreen mode Exit fullscreen mode

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)