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 ManagedServiceIdentityparameters.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)