Your Transform Is Code, So Test It Like Code

Pull reusable logic into a .dwl module, import it cleanly, and pin its behaviour with the dw CLI, MUnit, and golden-file tests — because a transform is code, and pure functions are a joy to test.

At some point a transform stops being a snippet and becomes an asset. Three flows compute the order total the same way, a fourth computes it slightly differently, and now you have a bug that only shows up on tax-exempt orders in one of them. The copy-paste caught up with you. The fix is the same one you’d reach for in any language — pull the shared logic into a module, and write a test that says what it’s supposed to do.

DataWeave is unusually good at both, and for the same reason it’s good at everything else: it’s a functional language with no hidden state. A function that takes a value and returns a value is the easiest thing in the world to reuse and to test. Let’s use that.

A module is just a .dwl of functions

A module is a DataWeave file with declarations and no body — no ---, no output, just %dw 2.0 and a set of funs (and vars and types) you want to share. Here’s one for the order math that keeps getting re-derived:

%dw 2.0

import sumBy from dw::core::Arrays

type LineItem = { sku: String, price: Number, qty: Number }

/**
 * Extended price for a single line.
 */
fun lineTotal(item: LineItem): Number = item.price * item.qty

/**
 * Total of every line on an order.
 */
fun orderTotal(items: Array<LineItem>): Number =
  items sumBy (i) -> lineTotal(i)

No trailing expression. A module doesn’t produce a value — it exposes definitions for other scripts to pull in. The type LineItem gives the functions a documented shape, and the doc comments carry through to tooling.

Where the file lives determines its import path. In a Mule project, DataWeave modules sit under src/main/resources, and the folder structure maps to the module name with :: between segments. Put the file above at src/main/resources/orders/OrderMath.dwl and it becomes the module orders::OrderMath.

Importing it three ways

From a transform, you reach the module the same way you reach the standard library — because to DataWeave there’s no difference between your module and dw::core::Strings. Import everything, import a name, or import a name under an alias:

%dw 2.0
// pull in one function by name
import orderTotal from orders::OrderMath
// or everything: import * from orders::OrderMath
// or aliased: import orderTotal as total from orders::OrderMath
output application/json
---
{
  id: payload.orderId,
  total: orderTotal(payload.items)
}

Selective imports keep the header honest about what a script actually depends on. import * is convenient while you’re exploring and worth tightening up before it ships.

When a module outgrows one project — the order math that four services all want — the MuleSoft-native home for it is Anypoint Exchange. You package the .dwl files as a reusable library and publish it as a versioned asset; other projects declare it as a dependency and import from it. That’s outside the language itself and firmly in build-and-publish territory, so I’ll leave the Maven coordinates to the Exchange docs — the point is that a DataWeave module is a distributable artifact, not a file you email around.

The fastest test loop: the dw CLI

Before any framework, there’s the command line. The dw tool runs a script against an input and prints the result, which is exactly the loop you want when you’re checking whether a function does what you think:

# inline script, no input needed
dw 'output application/json --- orders::OrderMath::orderTotal([{sku:"KB-01",price:79,qty:1}])'

# a real transform file, with a JSON payload piped in as `payload`
dw -i payload sample-order.json -f normalize-order.dwl

-i payload sample-order.json binds the file to the payload variable; -f runs a script file. This is the tightest feedback loop DataWeave has — faster than spinning up a Mule app, and it’s where I do most exploratory work alongside the Playground.

Pinning behaviour with a real test

The CLI tells you what a script does right now. A test tells you when that changes. Because the functions are pure, a DataWeave test is refreshingly literal: feed known input, assert on known output, done. DataWeave ships a testing framework in dw::test for exactly this — you describe a suite, give it cases, and run them with the CLI’s test runner. The shape is roughly:

%dw 2.0
import * from dw::test::Tests
import * from dw::test::Asserts
import orderTotal from orders::OrderMath
---
"OrderMath" describedBy [
  "sums the line items" in do {
    var items = [
      { sku: "KB-01", price: 79.0, qty: 1 },
      { sku: "MS-02", price: 29.5, qty: 2 }
    ]
    ---
    orderTotal(items) must equalTo(138.0)
  }
]

The exact assertion DSL has shifted across releases, so check it against your runtime’s version — but the idea is stable: a pure function, a fixed input, an expected value. No mocks, no setup, no teardown, because there’s nothing stateful to set up or tear down. That’s the payoff of a language with no mutation.

Golden files for whole transforms

Unit-testing a function is one thing; pinning the output of a full transform is another. For that, the durable pattern is a golden file — you commit the expected output next to a fixed input, and the test asserts the transform still produces it byte-for-byte. Inside a Mule project that assertion runs under MUnit, the runtime’s test framework, and the whole test is a few lines:

<munit:test name="normalize-order-produces-expected-json">
  <munit:execution>
    <flow-ref name="normalize-order"/>
  </munit:execution>
  <munit:validation>
    <munit-tools:assert-that
      expression="#[payload]"
      is="#[MunitTools::equalTo(
        readUrl('classpath://golden/order-normalized.json','application/json'))]"/>
  </munit:validation>
</munit:test>

readUrl(...) loads the golden JSON as data — not text — so the comparison is structural, and a reordered key or a reformatted number won’t produce a spurious failure. When the transform legitimately changes, you regenerate the golden file, eyeball the diff in review, and commit it. The diff is the change log for the mapping.

That’s the one place Mule plumbing earns its keep here — MUnit runs inside the runtime because it’s testing the flow the transform lives in. For the transform logic itself, the dw CLI and dw::test never leave the language.

Final thoughts

The reason DataWeave is worth testing is the same reason it’s worth learning as a language: it’s made of pure functions over immutable values, and that makes it the most testable layer in most integrations. A transform pulled into a named module, imported by three flows, and pinned by a golden file is not fancier than a pile of copy-pasted expressions — it’s just honest about being code. Treat it that way and the refactors stop being scary.

Next: When the Payload Is Bigger Than the Heap — streaming, deferred evaluation, and the operations that quietly load your gigabyte into memory.

Comments