DEV Community

Daniel Jonathan
Daniel Jonathan

Posted on

Workaround: Testing Logic Apps Data Mapper Maps on macOS

The Logic Apps Data Mapper in VS Code lets you create XML transformation maps on macOS. You can drag connections between source and target schemas, add functions, save — the visual designer works. The .lml file gets written to disk. You can even compile it to XSLT 3.0.

But you can't test it.

Mapping

When you click Test Map on macOS, you get this:

400 – BadRequest: The .NET framework worker could not be found.
Enter fullscreen mode Exit fullscreen mode

The Test Map feature depends on the Azure Functions .NET Framework worker — a Windows-only component. The button is there, the backend isn't. You can create maps all day — you just can't verify they produce correct output without deploying to Azure or switching to a Windows machine.

This post shows how to compile and run Data Mapper maps locally on any platform using the Logic Apps testing SDK and standard .NET unit tests.


The options

When you can't use Test Map, you have three paths:

  1. Deploy to Azure and test there — works, but slow and requires infrastructure for every iteration
  2. Start func host start locally with Azurite, call the REST compile endpoint — heavyweight, requires Azurite and a warm Azure Functions host, flaky on macOS
  3. Use DataMapTestExecutor from the testing SDK — no host, no Azurite, no Azure, just dotnet test

Option 3 is what this post covers.


DataMapTestExecutor

The Logic Apps testing SDK (Microsoft.Azure.Workflows.WebJobs.Tests.Extension v1.0.1) includes a class called DataMapTestExecutor. It does two things:

  1. Compiles LML to XSLTGenerateXslt(mapName) reads the .lml from Artifacts/MapDefinitions/, resolves schemas, custom functions, and inline XSLT snippets, and returns the compiled XSLT 3.0 as bytes.
  2. Runs the transformRunMapAsync(xslt, input) takes the compiled XSLT and input XML bytes, executes the transform via SaxonCS, and returns the result.

This is the same compilation and execution pipeline that the designer's Test Map button and the func host runtime use — you're running the same code path, just without the Azure Functions host scaffolding.


Setting up the test project

Project structure

The test project lives alongside the Logic App project:

MyWorkspace/
├── MyLogicApp/                     # Logic App project (Standard)
│   ├── Artifacts/
│   │   ├── MapDefinitions/         # .lml source files
│   │   ├── Maps/                   # compiled .xslt (generated)
│   │   ├── Schemas/                # XSD / JSON schemas
│   │   ├── SampleData/             # sample input XML
│   │   └── DataMapper/
│   │       └── Extensions/
│   │           ├── Functions/      # custom function XML files
│   │           └── InlineXslt/     # inline XSLT snippets
│   └── .vscode/
└── Tests/
    └── MyLogicApp.Tests/           # test project
        ├── MyLogicApp.Tests.csproj
        └── OrderToShipment/
            ├── OrderToShipmentTest.cs
            └── Order-input.xml     # test-specific input data
Enter fullscreen mode Exit fullscreen mode

The .csproj

PackageReference

The critical dependency is Microsoft.Azure.Workflows.WebJobs.Tests.Extension v1.0.1 — not v1.0.0. The DataMapTestExecutor class doesn't exist in v1.0.0.

Setup: project root, compilation, and result parsing

DataMapTestExecutor needs the Logic App project root — the directory that contains Artifacts/. It scans from there to find schemas, custom functions, and map definitions. [TestInitialize] runs before each test method — it compiles the LML to XSLT so every test starts from a freshly compiled stylesheet.

Initialize

The ProjectRoot path is relative from the test's output directory — the number of .. segments depends on your folder structure. Count from bin/Debug/net8.0/ up to wherever the Logic App project lives.

GenerateXslt takes the map name without path or extension. It finds the matching .lml in Artifacts/MapDefinitions/, reads the schemas from the LML header, loads custom functions, and compiles to XSLT 3.0.

RunMapAsync returns a JToken with the transform output encoded as base64:

{ "$content-type": "application/xml", "$content": "PFNoaXBtZW50cy4uLg==" }
Enter fullscreen mode Exit fullscreen mode

A helper to decode it:

private static XDocument ParseResult(JToken result)
{
    var base64 = result["$content"]!.Value<string>()!;
    var xml = Encoding.UTF8.GetString(Convert.FromBase64String(base64));
    return XDocument.Parse(xml);
}
Enter fullscreen mode Exit fullscreen mode

Map 1: Order to Shipment — simple field mappings and filtering

This map transforms a single Order into a Shipment. It tests custom extension functions (prefixShipRef, reformatDate, executionDateTime), direct field mappings, a $for loop over line items, and a $if filter that excludes items with Quantity < 1.

Testing individual fields

Each mapping requirement gets its own test for clear failure diagnostics:

Image33

Validating the entire output

One test that checks everything — structure, values, element order, and the filter:

CompleteMapping


Map 2: PurchaseOrders to ShippingManifest — attributes, nested loops, computed values

This is a more complex map. It transforms multiple PurchaseOrders into a ShippingManifest with XML attributes, nested $for loops (Orders → Items), computed aggregates (totalWeight, totalValue), and custom functions (shipId, formatAddress, hazardousFlag, parcelSubtotal).

The test class follows the same pattern as Map 1 — same setup, different map name and input file.

Testing attributes and computed values

Image2

Testing nested loop output with hazmat flag

The inner $for loops over each Order's Items. The hazardousFlag custom function maps @hazardous="true" to "Y":

Image3


Running the tests

UnitTestingExecution


What DataMapTestExecutor does under the hood

DataMapTestExecutor(projectRoot)
  → scans Artifacts/Schemas/ for XSD and JSON schemas
  → scans Artifacts/DataMapper/Extensions/Functions/*.xml for custom functions
  → scans Artifacts/DataMapper/Extensions/InlineXslt/*.xml for XSLT snippets

GenerateXslt(mapName)
  → reads Artifacts/MapDefinitions/{mapName}.lml
  → resolves schema references from the LML header
  → compiles LML → XSLT 3.0 via DataMapperUtility
  → returns byte[] of the compiled stylesheet

RunMapAsync(xslt, input)
  → loads the XSLT via SaxonCS (bundled in the SDK)
  → runs the transform against the input bytes
  → returns JToken with base64-encoded XML result
Enter fullscreen mode Exit fullscreen mode

Known gotchas

v1.0.0 vs v1.0.1. DataMapTestExecutor doesn't exist in v1.0.0 of the SDK. If dotnet test fails with a missing type, check your Microsoft.Azure.Workflows.WebJobs.Tests.Extension version.

ProjectRoot must point to the Logic App root. The path must resolve to the directory containing Artifacts/ — not the test project, not the solution root. If GenerateXslt throws "map not found", your path is wrong.

Zero-parameter custom functions. If any <function> in Functions/*.xml has no <param> elements, the SDK hits a NullReferenceException on function.parameters.Length. The entire XML file is silently skipped, making all functions in it "unrecognized". Workaround: add a dummy parameter <param name="_unused" as="xs:string"/>.

Custom function namespace scoping. Function files are parsed in isolation. Namespace prefixes declared in the main stylesheet (like xmlns:tns="...") are not available inside function XML files. Using tns:ElementName in a function's XPath will throw a NullReferenceException at init time.

Base64 response format. RunMapAsync does not return raw XML. The result is a JToken with $content containing base64-encoded output. You must decode it before asserting.


Beyond testing: the designer's save problem

And even if Test Map worked, the visual designer has its own problems — changes that revert on reopen, string literals that lose quotes, expressions that get silently rewritten. I wrote a separate post about editing LML files directly as a more reliable alternative to the designer.


The XSLT Debugger extension is on the VS Code Marketplace for macOS and Windows.

Top comments (0)