SysTest in D365 F&O: Unit Tests That Survive a Platform Update
Most F&O customizations ship with a demo script and a prayer. Then a platform update lands, a Microsoft hotfix changes a base method you wrapped with Chain of Command, and the first person who notices is a controller during period close. SysTest is the cheapest insurance you can buy — if you treat it like production code, not like a checkbox on the ISV checklist.
What SysTest actually is
SysTest is the X++ unit-testing framework that runs inside the AOS. A test class extends SysTestCase (or a more specific base like SysTestCaseDataDependent), methods prefixed with test are discovered, and assertions live on the case object: this.assertEquals, this.assertTrue, this.assertNotNull. It is not a UI robot. It does not click forms. If your "test" needs a user to open Sales order details, you have an integration rehearsal, not a unit test.
That distinction matters because F&O's cost of a failed save is paid in finance hours, not in red CI. The tests that earn their keep are the ones that pin your logic: number-sequence fallbacks, CoC wrappers, custom posting validations, XDS policy predicates, SysOperation controllers. Leave the standard SalesTable insert path to Microsoft's own regression; you will never out-test the platform.
Isolate the data, or the suite will lie
The classic failure mode is a test that inserts a CustTable against whatever already lives in the UAT company, then asserts a count. After three sprints the count is wrong, someone "fixes" the assertion, and the test now documents the mess.
Use a dedicated test company or a fixture that creates and deletes in setUp / tearDown. Prefer ttsbegin / ttsabort when the engine allows it so leftover rows never escape. For tables with number sequences, call the sequence API explicitly with a known format rather than hoping the next value is SO-000123. If two tests share a unique index (item id, journal name, dimension combination), they will flake the first time they run in parallel on a build VM.
[SysTestMethod]
public void testOverCreditLimitBlocksConfirm()
{
SalesTable sales = this.createOpenOrderAboveLimit();
ttsbegin;
sales.CreditLimitCheck = NoYes::Yes;
this.assertFalse(sales.canConfirm(),
"Orders over the customer limit must not confirm.");
ttsabort;
}
The assertion message is not decoration. When this fails at 2 a.m. after a PU, the message is the only design doc the on-call engineer will read.
Skip the form, test the method
Form event handlers are awkward to instantiate in SysTest. If the real logic lives in clicked on a button, extract it to a class method the handler calls — then test the class. This is the same move as "skinny controllers" in any web stack, and it is the single refactor that makes CoC wrappers testable.
For SysOperation batch jobs, do not assert that a batch task was created. Instantiate the service class, call run(), and assert the table state. Batch infrastructure has its own timing; coupling to it turns a 200ms test into a 15-second wait.
What to pin against platform updates
A useful SysTest suite is a list of invariants you own:
- CoC
nextstill runs: if you wrapSalesLine.reserveNow, a test that reserves a known quantity and checksInventTransis the tripwire when Microsoft changes the method signature or the call order. - Feature-management branches: if your code behaves differently when a Microsoft feature is enabled, have two tests, not one test that follows whichever flag UAT happens to have.
- Data entities used by DMF or dual-write: insert through the entity and read the staging fields you map. Mapping drift is silent until a nightly job dumps 40,000 errors.
Run the suite on the build VM against the target PU before you apply it to UAT. A red suite on the build is a gift; a red period-close is not.
Keep the runtime honest
A 40-minute SysTest pass will be skipped. Cap each test in tens of milliseconds to low seconds. Mock external HTTP (custom services calling Azure) behind an interface. Do not start a full MRP in a unit test. Tag slow tests and run them nightly, not on every check-in.
If you only add one test this quarter, add the one that failed in production last time. SysTest does not need coverage theatre. It needs a growing list of things that already hurt once and must not hurt twice.
Keep reading
Dual-Write Dropped My Field and Reported Success
Why a dual-write map can complete with zero errors while a custom F&O field never lands in Dataverse — staging tables, unpublished maps, and change tracking after a schema change.
OData 429s from F&O: What Actually Throttles You
HTTP 429 from Dynamics 365 Finance & Operations is usually the query shape, not the request count — cross-company scans, $filter that cannot seek, $batch that still burns SQL, and why retrying harder makes month-end worse.
Feature Management in D365 Finance and Operations: Flags, States, and Custom Features
How the D365 F&O Feature management workspace works, what feature states mean for ALM, and how to ship your own X++ features behind flags.
D365 F&O Data Entities: Composite, Computed, and Virtual Fields Explained
Data entities look like simple tables until you need a nested import, a derived column, or a field with no storage. Knowing the three advanced shapes is what makes integrations actually work.
Extending Data Entities in D365 Finance & Operations Without Breaking Upgrades
Add fields, computed columns, and validation to standard D365 Finance & Operations data entities the upgrade-safe way — with X++ examples and the staging-table traps to avoid.
Chain of Command vs Event Handlers: Extending D365 F&O the Right Way
When to use Chain of Command and when to use pre/post event handlers in Dynamics 365 Finance & Operations — with X++ examples, a decision table, and the gotchas that trip up teams.
Newsletter
New posts, straight to your inbox
One email per post. No spam, no tracking pixels, unsubscribe anytime.
Comments
- No comments yet. Be the first.