Best Practices: Testing
Design for verifiability
A payroll regulation that cannot be tested reliably is a liability. The expected result of every calculation should be derivable from the inputs, and the test setup should be self-contained enough to run independently of environment state.
In practice: design employee scenarios in complementary pairs — one triggers a condition, the other does not — so every branch is exercised. Assert collector totals alongside individual wage type results. For retro scenarios, assert the historical recalculated values explicitly, not just the current period. A test that only checks happy-path results leaves the most complex cases untested.
Temporal Test Data
Use createdObjectDate to anchor all test objects in the past
When a test operates on historical periods, every object imported via
Exchange (tenants, employees, cases, regulations) gets a created timestamp.
If that timestamp is not set explicitly, the engine uses the current date.
An object created after the payrun evaluation date is invisible to that
run — the engine behaves as if it does not exist. This is the most common
reason a test that was written in the past produces empty or missing results
when run on a later date.
In practice: set createdObjectDate at the root level of the Exchange
file. This single date is applied to every object in the import that does
not carry an explicit created field. Choose a date that is safely before
the earliest evaluation date in the test.
createdObjectDate: "2025-01-01T00:00:00.0Z"
tenants:
- ...
Mid-period case changes require an explicit created date
createdObjectDate applies uniformly to every object. Case changes that
represent events occurring after the baseline would inherit the root
date and collide with the initial values, causing a duplicate entry error on import.
In practice: give each mid-period case change an explicit created
field that places it within the target pay period — after the period start
and before the evaluation date.
caseFieldName: Salary
value: "5600"
start: "2025-02-10T00:00:00.0Z"
created: "2025-02-07T00:00:00.0Z"
The start date defines when the value becomes effective in the payroll
calculation. The created date defines when the change was entered into
the system and therefore which evaluation dates can see it.
created must be ≤ evaluation date, start controls effectivity
These are two independent temporal axes:
| Field | Controls | Analogy |
|---|---|---|
created |
System visibility — is this value known at evaluation time? | HR enters the change in the system on this date |
start |
Payroll effectivity — from which date is the value used in calculation? | The contractual start date of the change |
A case value with start: 2024-01-01 and created: 2025-02-15 is active
in the January 2024 pay period but only becomes visible from evaluation
date 2025-02-15 onwards. This is the standard engine pattern for
late-entered payments that must be attributed to a past period.
Duplicate entry errors indicate a created date collision
The engine enforces uniqueness on the combination of employee, case field,
value type, and created timestamp. If two case changes for the same field
land on the same created date, the import fails with a Duplicate entry error.
In practice: when a duplicate entry error appears on import, check
whether any case field has more than one change without an explicit
created date. The fix is always to give the later change a distinct
created date, not to alter start or createdObjectDate.
Case value visibility uses strict <; wage type derivation uses <=
Two different comparison operators apply at evaluation time:
| Object | Visibility condition | Effect at created == evalDate |
|---|---|---|
| Case value | created < evaluationDate (strict) |
Not visible — value is excluded |
| Wage type / Lookup | created <= evaluationDate |
Visible — object is derived |
This asymmetry is intentional: a case value represents a change entered by HR and is treated as not yet propagated on the same day; a wage type or lookup is a regulation artifact that takes effect immediately on its creation date.
In practice: when writing edge-date tests, set the expected value for a
case field with created == evaluationDate to 0 (not visible), and the
expected result for a wage type with created == evaluationDate to its
calculated value (visible).
end on Period and CalendarPeriod values is inclusive
The end field of a case value defines the last active moment of that value.
The engine treats end as inclusive: a value with end: 2024-03-01T00:00:00Z
is still active on March 1, producing a pro-rata split for CalendarPeriod fields.
In practice: to stop a value at the end of a calendar month without
overlapping into the next, set end to the last day of the month (e.g.
2024-01-31T00:00:00Z to end in January). Do not set end equal to the
successor's start.
For open-ended CalendarPeriod values, created determines priority; start breaks ties
When multiple CalendarPeriod values overlap for the same case field, the engine selects the active value by:
- Latest
created— the value entered most recently into the system wins. - Latest
start— if two values have the samecreatedtimestamp, the one with the later period start wins.
| Value | start | created | Result for Feb |
|---|---|---|---|
| Salary 3 000 | Feb 1 | Jan 15 | — loses |
| Salary 4 000 | Jan 1 | Feb 15 | wins (later created) |
In practice: when a new salary supersedes an earlier one, the
deciding factor is created, not start. A retro correction entered
later (higher created) will override a value with a later start.