Skip to content

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:

  1. Latest created — the value entered most recently into the system wins.
  2. Latest start — if two values have the same created timestamp, 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.


See Also