bcquality/microsoft/knowledge/testing/transactionmodel-attribute-governs-test-transactions.md
Michael Dieringer 830eaff68f
3 AL/BC testing patterns from an external BC testing expert's blog (Luc van Vugt, fluxxus.nl) (#159)
* Add 4 more AL/BC testing patterns from Luc van Vugt's fluxxus.nl blog

Fourth batch from CURABIS ApS, mined from an external BC/NAV testing expert's blog archive (fluxxus.nl). Confirm+StrSubstNo interaction with ConfirmHandler, Table Relation Test's OnAfterRemoveTableRelation exclusion hook (verified against BCApps source, codeunit 134926), committing shared lazy-Initialize fixture data, and Assert.IsFalse vs asserterror for boolean checks.

* Address Jesper Schulz-Wedde's review on PR #159

- transactionmodel-attribute-governs-test-transactions.md: the "Commit
  causes an error" behavior is specific to an explicitly declared
  AutoRollback attribute. A test method with no TransactionModel
  attribute at all is a distinct, valid shape — BCApps' own
  codeunit 134915 "ERM Online Mapping Setup" commits inside a lazy
  Initialize() with no attribute declared, cleaning up via a manual
  asserterror at the end. Evidence for commit-shared-test-fixture-
  inside-lazy-initialize.md (this PR), which is correct as submitted.
- confirm-needs-strsubstno-before-confirmhandler-sees-substituted-text.md:
  reframe as a known, unconfirmed-fix platform defect
  (microsoft/ALAppExtensions#23935) rather than designed behavior; add
  the Message/MessageHandler asymmetry as supporting evidence.
- table-relation-test-exclude-known-invalid-relations-via-event.md:
  note the test-app-only consumer dependency; correct "walks every
  TableRelation field property in the app" to the actual tenant-wide
  Table Relations Metadata scope across installed apps.
- Wire confirm-needs-strsubstno, commit-shared-test-fixture-inside-
  lazy-initialize, and table-relation-test-exclude-known-invalid-
  relations-via-event into al-testing-review.md's candidate-selection
  cues.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Address second round of Jesper Schulz-Wedde's review on PR #159

- commit-shared-test-fixture-inside-lazy-initialize.md: fundamentally
  rewritten. AutoCommit is the documented default TransactionModel, not
  AutoRollback. Explains the real mechanism (Commit() protects a fixture
  from the test method's own later deliberate rollback, per Codeunit.Run/
  TransactionModel-property semantics) and the TestIsolation dependency
  (Disabled/Codeunit survive across methods, Function does not). Fixtures
  rewritten to demonstrate the actual failure/success shape.
- transactionmodel-attribute-governs-test-transactions.md: now states the
  AutoCommit default explicitly and agrees with the article above, closing
  the contradiction Jesper flagged between the two testing articles.
- Deleted confirm-needs-strsubstno-before-confirmhandler-sees-substituted-text
  (.md/.good.al/.bad.al): the underlying platform bug (microsoft/
  ALAppExtensions#23935) was closed as completed in Feb 2024; cannot be
  reproduced or bc-version-pinned on any currently supported version.
- table-relation-test-exclude-known-invalid-relations-via-event.md: added
  the [Scope('OnPrem')] boundary verified against BCApps' Table Relation
  Test codeunit.
- use-assert-isfalse-not-asserterror-for-boolean-checks.md: added a Scope
  section resolving the overlap with asserterror-needs-expectederror-and-code.
- al-testing-review.md: fixed the shared-fixture cue to catch the actual
  anti-pattern instead of the compliant shape, added the missing cue for
  use-assert-isfalse-not-asserterror-for-boolean-checks, wired precedence
  between it and the generic asserterror rule, and removed the cue for the
  deleted article.
- Added in-file Source provenance (specific fluxxus.nl post per article,
  with what was independently verified vs. taken from the post) to the
  three surviving externally-inspired articles, per Jesper's request that
  provenance live in the knowledge file itself, not only the PR description.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Fix remaining correctness issues from Jesper's 2026-09-15 re-review

- commit-shared-test-fixture-inside-lazy-initialize: three sub-issues.
  Recommended TestIsolation = Codeunit instead of listing Disabled as an
  equal option - Disabled never rolls back at all ("tests are not
  isolated from each other" per the property's own docs), so a fixture
  this pattern commits under Disabled is permanent database
  contamination unless something else tears it down; Disabled is now
  only mentioned alongside that explicit teardown requirement. Added
  precedence in al-testing-review.md so the deliberate end-of-test
  asserterror Error(...) rollback sentinel isn't also flagged by the
  generic asserterror-needs-expectederror-and-code rule. Rewrote both
  fixtures to actually demonstrate the pattern: persisted fixture data
  (an Item record) instead of an empty comment, a second [Test] method
  that depends on the fixture surviving into it, and an explicit
  Subtype = TestRunner / TestIsolation = Codeunit runner codeunit.

- table-relation-test-exclude-known-invalid-relations-via-event:
  the length/type rule was stated as one global requirement. Verified
  ValidateFieldRelation in codeunit 134926 directly (BCApps reference
  clone) and split it into the two branches the source actually has:
  a field with any unconditional relation needs exact length and exact
  resolved type; a field whose relations are all conditional only fails
  on being shorter (longer is fine) than the largest related field, and
  when the required type is specifically Code, a Text source passes too
  - a tolerance that does not apply on the unconditional side and does
  not extend to a required Text.

Rebased onto upstream/main (one conflict in
transactionmodel-attribute-governs-test-transactions.md - upstream had
already linked its sample references via the READ convention, ours
added a Source section; merged both). Also converted the 3 remaining
plain-backtick sample references in this PR to the READ-convention
markdown-link form, same fix as #156/#157/#158.

* Fix four merge-critical issues from Jesper's 2026-09-22 review

- al-testing-review.md: the generic ExpectedError cue's asserterror
  Assert.IsTrue/IsFalse exclusion was unconditional, but the
  specialized rule it deferred to only claims the pure-inversion
  shape. A test expecting the guarded Boolean-returning call itself to
  raise fell through both routes. Narrowed the exclusion to the same
  inversion-only condition the specialized cue already uses.
- asserterror-needs-expectederror-and-code.md: the rollback-sentinel
  exception (a trailing asserterror Error(...) used purely to force a
  fixture rollback, not to verify a specific failure) previously lived
  only in skill routing prose. Encoded it directly in the article's
  Anti Pattern section so every consumer of the knowledge base sees it,
  not just this one skill.
- commit-shared-test-fixture-inside-lazy-initialize.good.al/.bad.al:
  replaced hand-rolled Item.Init()/Insert(true) with
  LibraryInventory.CreateItem, so the canonical fixture doesn't itself
  trigger use-library-codeunits-for-test-fixtures.
- table-relation-test-exclude-known-invalid-relations-via-event.good.al/
  .bad.al: declared minimal "Sample Setup"/"Sample Header" tables
  inline instead of referencing undefined symbols, matching this
  repo's own convention that every fixture is self-contained.

* Give the table-relation-test fixtures a real relation to exclude and one to protect

The "Category Code" field had no TableRelation at all, so the good
subscriber's RemoveTableRelation call targeted metadata that never
existed - a no-op. Added a real TableRelation to "Sample Setup" on
that field (the one known exception to exclude) and a second,
ordinary self-referencing relation ("Parent No." -> "Sample
Header"."No.") with no exception. The good fixture now removes only
the first; the bad fixture's table-wide removal (field/related
table/field all 0) now demonstrably also strips the second, showing
the actual anti-pattern instead of removing nothing meaningful.

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Jesper Schulz-Wedde <jesper.schulzwedde@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-29 17:13:06 +02:00

4.6 KiB

bc-version domain keywords technologies countries application-area
all
testing
transactionmodel
attribute
test
autorollback
autocommit
testisolation
al
w1
all

Match TransactionModel to the commit behavior of the code under test

Description

[TransactionModel(...)] declares how a test method interacts with the database's write transaction. The attribute applies only to methods inside a codeunit with SubType = Test and takes one of three values: AutoRollback, AutoCommit, or None. AutoCommit is the documented default — a test method with no [TransactionModel(...)] attribute at all runs under AutoCommit, not AutoRollback and not None (Microsoft's TransactionModel property reference states this explicitly: "AutoCommit is the default value"). The "a call to Commit produces a runtime error" behavior is specific to the explicitly declared AutoRollback attribute. BCApps' own canonical pattern for a lazily-initialized shared fixture (see codeunit 134915 "ERM Online Mapping Setup") declares no TransactionModel attribute at all — so it runs under the AutoCommit default — calls Commit() inside its Initialize() helper, and cleans up manually with a deliberate asserterror Error(...) at the end rather than relying on automatic rollback; this is a legitimate, common pattern, not a bug. Per the same reference, under AutoCommit an error, even one caught by asserterror, still rolls back the transaction — but "only to the point at which Commit was called" if the code being tested committed first. When a test method does declare AutoRollback explicitly, the choice must match the code being exercised: per the platform reference, "if the code that you test includes calls to the COMMIT Method, then set the TransactionModel property on the test method to AutoCommit." Applying AutoRollback to a test that drives code which calls Commit produces a runtime error on the first Commit, not a meaningful assertion failure.

Best Practice

Leave [TransactionModel(...)] undeclared to get the AutoCommit default when the codeunit's own tests rely on that default's behavior — for example a lazily-initialized shared fixture that commits once and cleans up its own scratch changes with a manual asserterror-based rollback (see commit-shared-test-fixture-inside-lazy-initialize.md); do not treat that absence as equivalent to declaring AutoRollback. When declaring [TransactionModel(...)] explicitly instead, pick AutoRollback for a test whose own logic and the code it exercises make no Commit call, AutoCommit when the code under test genuinely calls Commit — posting routines, job-queue handlers, integration flows — and make the test exercise that commit path, and None for a read-only test or one that drives UI code without writing from the test method itself. Pair an intentional, suite-wide reliance on AutoCommit with a TestIsolation-enabled test runner so committed changes are reverted at a higher scope.

See sample: transactionmodel-attribute-governs-test-transactions.good.al.

Anti Pattern

Declaring [TransactionModel(AutoRollback)] explicitly on a test method without checking whether the tested business logic calls Commit. The test throws at the first Commit, leaving no verdict on the behavior it intended to verify; in a CI run this looks like a flake or a setup bug, not a specification mismatch. The mirror-image anti-pattern is defaulting to AutoCommit across the suite "to avoid the error" — without a TestIsolation runner this permanently dirties the test database between runs and produces order-dependent test outcomes. Flagging a Commit() call in a test method that declares no TransactionModel attribute at all is not this anti-pattern — that shape does not error, and is BCApps' own documented pattern for shared lazy fixtures.

See sample: transactionmodel-attribute-governs-test-transactions.bad.al.

Source

The AutoCommit-is-default claim and the exact rollback-to-last-Commit mechanics are quoted from Microsoft's TransactionModel Property reference: https://learn.microsoft.com/en-us/previous-versions/dynamicsnav-2018-developer/TransactionModel-Property. The current AL TransactionModel attribute page describes the same three values but never states a default; this older property reference is the citable source for that fact.