Anaplan model builders have borrowed a lot from software engineering over the last few years. ALM gave us source and target environments, revision tags, and something close to a branch-and-release workflow. Data Orchestrator gave us pipelines that look like real ETL. What most teams still have not borrowed is the part that makes any of it safe: automated tests.
The typical Anaplan release still goes like this. A builder changes a formula in DEV, clicks through two or three dashboards, decides it looks right, tags a revision, and syncs to PROD. Three weeks later someone in FP&A notices that the constant-currency variance column has been wrong since the last release, and nobody can say which change broke it.
You can fix this inside Anaplan. You do not need an external test framework, and you do not need to wait for a vendor feature. What you need is a small, deliberately-built test harness module set that lives in the model, runs on every release, and fails loudly. This tutorial walks through building one.
It assumes you are comfortable with lists, line item subsets, SUM/LOOKUP, saved views, and the basics of ALM revision tags. If your release process itself is shaky, sort that out first — a test harness on top of an undisciplined sync habit just produces noise.
The three kinds of test worth building
Do not try to test everything. Anaplan models have thousands of cells that are nobody's business. Target three categories.
- Invariant tests (tie-outs). Statements that must be true in any dataset, at any time. Balance sheet assets equal liabilities plus equity. Allocated cost equals source cost. Supply plan receipts equal netted requirements. These are cheap to build and catch the majority of real breakages.
- Regression tests (golden datasets). A frozen input dataset plus its known-good outputs. After any change, recalculate and compare. Catches the subtle cases: a changed
SELECT, a time range shifted by one period, a summary method flipped from Sum to Formula. - Structural tests (guardrails). Assertions about the model itself rather than its numbers: no orphaned list members, no mappings pointing at
Unassigned, no dimension with zero active members, no user without a role.
Unit-testing every formula is not the goal. The goal is that no release can silently change a number a human relies on.
Step 1: A test registry list
Create a flat list, Tests, where each member is one named assertion. Give it properties so that the harness is self-documenting.
| Property | Type | Purpose |
|---|---|---|
Test Name | Text | Human-readable description of the assertion |
Category | List (Test Categories) | Invariant / Regression / Structural |
Owner | List (Users) | Who investigates a failure |
Severity | List (Severities) | Blocker / Warning / Info |
Tolerance | Number | Absolute tolerance for rounding, usually 0.01 |
Active | Boolean | Lets you retire a test without deleting history |
A registry list rather than a pile of line items matters for one reason: it makes the harness dimensional. One module, one set of formulas, n tests — and adding a test later is a list row plus a formula branch, not a new module.
Severity is worth defining carefully. Blocker means the release does not go. Warning means a human signs off explicitly. Info means we are watching a metric, not gating on it. Teams that make every test a blocker end up disabling the harness within two months.
Step 2: The expected-vs-actual module
Build TST01 Test Results, dimensioned by Tests and by nothing else if your tests are model-wide, or by Tests and a single coarse dimension (entity, or time at year level) if you want failures localised. Resist the urge to dimension it by your full hierarchy; a test harness that itself takes thirty seconds to calculate will not be run.
Line items:
Actual(Number) — the value produced by the model under test.Expected(Number) — the value the test asserts.Variance=Actual - Expected.Passed(Boolean) =ABS(Variance) <= Tolerance.Status(Text) =IF NOT Active THEN "SKIPPED" ELSE IF Passed THEN "PASS" ELSE "FAIL".Detail(Text) — a formatted message, e.g.Test Name & ": actual " & TEXT(Actual) & " vs expected " & TEXT(Expected).
The Actual line item is where the wiring happens. Use a single formula with a SELECT-per-test branch:
IF ITEM(Tests) = Tests.BS_Balances THEN 'FIN03 Balance Sheet'.Total Assets[SELECT: Time.'FY26']
ELSE IF ITEM(Tests) = Tests.Alloc_Ties THEN 'ALL02 Allocation Out'.Allocated Cost[SELECT: ...]
ELSE IF ITEM(Tests) = Tests.Supply_Nets THEN ...
ELSE 0
This is not elegant and experienced builders flinch at it. Build it anyway. The alternative — a line item subset that maps tests onto arbitrary source modules — is cleaner in theory but requires every tested measure to sit in one module, which they never do. The branch formula is explicit, greppable, and easy for the next builder to extend.
For invariant tests, Expected is usually a formula too: the other side of the identity. For BS_Balances, Expected is total liabilities plus equity. Variance then is the tie-out difference, and you get reconciliation reporting for free.
Step 3: Golden datasets for regression tests
Invariant tests are the easy half. Regression tests need frozen inputs.
The pattern that works: pick a small, representative slice of real data — one legal entity, one product family, three periods — and store it inside the model as a static dataset rather than importing it each cycle.
- Create a
Test Scenariomember alongside your real versions or scenario list. Never use a real version; you do not want test data insideActual. - Build
TST02 Golden Inputs, dimensioned like your driver input modules but restricted to the test slice, with the values hard-entered once and then locked with Dynamic Cell Access so nobody 'fixes' them. - Point your calculation engine at the test scenario. If your model already supports scenarios properly, this costs nothing — the calculation chain runs on
Test Scenariothe same way it runs onBudget. - Build
TST03 Golden Outputsholding the known-good results, captured once from a release you trust, again hard-entered and locked. - Wire the relevant
Testsmembers soActualreads the live calculated test-scenario output andExpectedreadsTST03.
The discipline point: golden outputs are only allowed to change deliberately. When a change legitimately alters a result, a human updates TST03 and records why. Make that a step in your release notes. The moment builders update golden outputs to make a red light go green, the harness is decorative.
Keep the slice small. A golden dataset covering ten thousand SKUs is a second production model. Three products, two entities, and a handful of periods will catch a mis-scoped SELECT just as reliably.
Step 4: Structural guardrails
These catch the failures that have nothing to do with formulas.
- Orphan check. In a module dimensioned by your lowest-level list,
Is Orphan = ISBLANK(PARENT(ITEM(Cost Centres))), summed to a count. Expected: 0. - Unassigned mapping check. Count members whose mapping property points at the
Unassignedcatch-all. Expected: 0, or a documented allowance. - Empty dimension check. For each critical list, count active members; assert greater than zero. Sounds paranoid until an import clears a list and every downstream total quietly reads zero.
- Data recency check.
Days Since Last Load = Current Date - Last Load Date. Assert less than your SLA. This one alone prevents a surprising number of 'the forecast looks wrong' escalations, because the real answer is usually that the feed died on Friday. - Role and access check. Count users with no role assigned, or with workspace admin who should not have it. Expected: 0. Pairs well with a security review cadence.
Structural tests are usually Warning severity rather than Blocker, with the exception of the empty dimension check, which should stop a release cold.
Step 5: The test results board
Build one UX page called Model Health. It has three things on it and nothing else.
- A headline KPI card:
Failing Blockers, a count. Green at zero, red otherwise. Conditional formatting, large font. - A grid of the
Testslist filtered toStatus = "FAIL", showingTest Name,Category,Owner,Severity,Variance,Detail. - A small history chart from the run log below.
The grid should be empty most days. An empty grid is the entire point; a wall of amber rows nobody reads is the failure mode.
Give the page to model builders, the model owner, and the FP&A lead who owns the numbers. Not to every planner — they do not need to know about your structural guardrails, and a red light with no context erodes trust in the model.
Step 6: Logging runs so you can see drift
A live test module tells you the state now. You also want history, so you can answer 'when did this start failing?'
Create a Test Runs numbered list, and a module TST04 Run Log dimensioned by Test Runs and Tests with line items Status, Variance, Run Date, Run By, Revision Tag. Add an action that copies current results into the next available run slot, and wire it into an action group called Release Checks along with the recalculation of the test scenario.
One process, one button: Run Model Tests. Ask anyone to run a fourteen-step checklist before a sync and they will run it twice and then stop.
Cap the run list — 200 runs is plenty — and cycle it. Unbounded log lists are how test harnesses end up as the largest module in the workspace.
Step 7: Making it a release gate
Now connect the harness to ALM.
The release process becomes:
- Development complete in DEV.
- Run Release Checks in DEV. All blockers pass, warnings signed off.
- Create the revision tag. Name it with the run ID, e.g.
R2026.02.14-TST112. - Sync to TEST or PROD.
- Run Release Checks again in the target, after sync.
Step 5 is the one people skip, and it is the one that earns its keep. Passing in DEV proves the logic is right against DEV data. Passing in PROD proves the sync landed and production data still satisfies the invariants. Structural and recency tests in particular behave completely differently across environments, because production data volumes and load schedules are different.
If you drive syncs through the REST API from a scheduler, you can go further: call the test action, export the results view, and fail the pipeline job on a non-zero blocker count. That is a genuine CI gate for a planning platform, and it is maybe thirty lines of scripting on top of the model work above.
Step 8: Feeding UAT instead of replacing it
A harness does not remove the need for user acceptance testing. It changes what UAT is for.
Without a harness, UAT users spend their time finding arithmetic breakages — which is expensive, slow, and demoralising, because they are doing regression testing by hand with no memory between cycles. With a harness, arithmetic breakages are caught before users see the model, and UAT can focus on the things only a user can judge: does the process flow make sense, is the page usable at month-end pace, are the drivers the ones the business actually manages, is the output something a board will accept.
Practically, give your UAT group two artefacts: the Model Health page (so they can see the model is internally consistent) and a scenario script (so they test process, not cells). Log their findings as new Tests members where they are assertable. Over three or four releases the harness accretes exactly the checks your business cares about, which is a much better test suite than anything designed up front.
What this costs and what it saves
A first harness — registry, results module, a dozen invariant tests, three or four structural guardrails, one UX page, a run log, and the action group — is two to four days of builder time on a mature model. A golden dataset for a major calculation chain is another day or two, mostly spent choosing a slice small enough to be cheap and rich enough to be meaningful.
Against that: the cost of one silent number error discovered a quarter late, in a model that feeds a board pack or a commission payment. That is a credibility event, and credibility is the thing that makes a planning platform worth owning.
The models we see survive five years all have some version of this. The ones that get quietly abandoned almost never do.
Where to go next
If you are building this into an existing estate, sequence it: invariant tie-outs first (highest catch rate per hour of build), then structural guardrails, then golden datasets for your single most business-critical calculation chain. Do not attempt full regression coverage in one pass.
If you would rather not do the first pass alone, our team builds test harnesses and release gates as part of Anaplan support and maintenance engagements, and as a standard step in performance optimization and platform migration work — a migration without regression tests is a rewrite with a hope attached. Get in touch if you want a second pair of eyes on your release process.