-
Notifications
You must be signed in to change notification settings - Fork 116
4 AL/BC testing patterns from an external BC testing expert's blog (Luc van Vugt, fluxxus.nl) #159
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Michael Dieringer (MichaelDieringer)
wants to merge
3
commits into
microsoft:main
Choose a base branch
from
Curabis:community-contribution/vanvugt-blog-patterns
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
34 changes: 34 additions & 0 deletions
34
microsoft/knowledge/testing/commit-shared-test-fixture-inside-lazy-initialize.bad.al
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| codeunit 50142 "Sample Test Library" | ||
| { | ||
| Subtype = Test; | ||
|
|
||
| var | ||
| Initialized: Boolean; | ||
| RollBackMsg: Label 'Revert back the tables to their original state.'; | ||
|
|
||
| local procedure Initialize() | ||
| begin | ||
| if Initialized then | ||
| exit; | ||
|
|
||
| CreateSharedFixtureData(); | ||
| Initialized := true; | ||
| end; | ||
|
|
||
| local procedure CreateSharedFixtureData() | ||
| begin | ||
| // insert master/setup data shared across every test in this codeunit | ||
| end; | ||
|
|
||
| [Test] | ||
| procedure FirstTestUsesSharedFixture() | ||
| begin | ||
| Initialize(); | ||
|
|
||
| // exercise/verify against the shared fixture, then make scratch changes of its own | ||
|
|
||
| asserterror Error(RollBackMsg); | ||
| // the deliberate rollback above also erases the never-committed fixture; | ||
| // Initialized still reads true on the next test, but the rows are gone | ||
| end; | ||
| } |
33 changes: 33 additions & 0 deletions
33
microsoft/knowledge/testing/commit-shared-test-fixture-inside-lazy-initialize.good.al
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| codeunit 50142 "Sample Test Library" | ||
| { | ||
| Subtype = Test; | ||
|
|
||
| var | ||
| Initialized: Boolean; | ||
| RollBackMsg: Label 'Revert back the tables to their original state.'; | ||
|
|
||
| local procedure Initialize() | ||
| begin | ||
| if Initialized then | ||
| exit; | ||
|
|
||
| CreateSharedFixtureData(); | ||
| Commit(); | ||
| Initialized := true; | ||
| end; | ||
|
|
||
| local procedure CreateSharedFixtureData() | ||
| begin | ||
| // insert master/setup data shared across every test in this codeunit | ||
| end; | ||
|
|
||
| [Test] | ||
| procedure FirstTestUsesSharedFixture() | ||
| begin | ||
| Initialize(); | ||
|
|
||
| // exercise/verify against the shared fixture, then make scratch changes of its own | ||
|
|
||
| asserterror Error(RollBackMsg); | ||
| end; | ||
| } |
34 changes: 34 additions & 0 deletions
34
microsoft/knowledge/testing/commit-shared-test-fixture-inside-lazy-initialize.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| --- | ||
| bc-version: [all] | ||
| domain: testing | ||
| keywords: [initialize, isinitialized, shared-fixture, commit, autocommit, asserterror, testisolation, lazy-initialization] | ||
| technologies: [al] | ||
| countries: [w1] | ||
| application-area: [all] | ||
| --- | ||
|
|
||
| # Commit shared fixture data created inside a lazy Initialize(), or later tests lose it | ||
|
|
||
| ## Description | ||
|
|
||
| A test method with no `[TransactionModel(...)]` attribute defaults to `AutoCommit` (see `transactionmodel-attribute-governs-test-transactions.md`): a method that completes without error commits automatically at its own boundary, with no explicit `Commit()` needed. So a lazy/shared `Initialize()` — guarded by an `IsInitialized` flag, creating master/setup data once to avoid repeating expensive setup across many `[Test]` methods — does not need `Commit()` just to survive into the next test method; under the default model it already will. (Declaring `[TransactionModel(AutoRollback)]` instead is not compatible with this pattern at all: `AutoRollback` assumes the code under test never commits, and a `Commit()` call under it raises a runtime error.) | ||
|
|
||
| What an early `Commit()` inside `Initialize()` actually guards against is the test method's *own later, deliberate* rollback — the BCApps cleanup idiom of ending a test with `asserterror Error(SomeLabel)` to undo demo-data mutations that method made, so the run doesn't permanently dirty the database. Per the documented `Codeunit.Run` transaction semantics, changes are committed at the end of an execution "unless an error occurs" — an unhandled error rolls back whatever wasn't already committed. `Commit()` closes out the fixture's own transaction immediately, so it is unaffected by whatever the rest of that method does afterward, including that end-of-test error. Without the early `Commit()`, the same deliberate rollback wipes out the fixture too, even though `IsInitialized` still reads `true` on the next test, since it's a plain variable, not persisted data. BCApps' `codeunit 134915 "ERM Online Mapping Setup"` shows exactly this shape: no `TransactionModel` attribute, `Commit()` inside a lazy `Initialize()`, and the test itself ends with `asserterror Error(RollBackMessage)`. | ||
|
|
||
| Protecting the fixture from that same-method rollback is necessary but not sufficient for the fixture to reach a *later* test method — that also depends on the executing test runner's `TestIsolation`. Under `Disabled` (the property's own documented default) or `Codeunit` (used by BCApps' own `TestRunner`, `CLITestRunner`, and `SnapTestRunner` codeunits), nothing rolls back until the whole test codeunit finishes, so the already-committed fixture survives across every method run before then. Under `Function`, the runner rolls back all database changes — explicitly including ones already committed via `Commit()` — after every single test method; no amount of committing inside `Initialize()` makes a fixture shared across methods survive that regime, because the whole premise of a lazy, once-per-codeunit fixture doesn't hold when every method is isolated from every other. | ||
|
|
||
| ## Best Practice | ||
|
|
||
| When a test method's own cleanup relies on ending in a deliberate error to roll back its scratch changes, call `Commit()` once, inside the lazy `Initialize()` guard, right after the shared fixture is created — before that cleanup-triggering error can run. This pattern only delivers a fixture shared across test methods when the executing runner's `TestIsolation` is `Disabled` or `Codeunit`; do not recommend it, or pair it with, a `Function`-isolated runner — that configuration undoes the committed fixture after every method regardless. | ||
|
|
||
| See sample: `commit-shared-test-fixture-inside-lazy-initialize.good.al`. | ||
|
|
||
| ## Anti Pattern | ||
|
|
||
| A shared `Initialize()` guarded by `IsInitialized` that creates fixture records without committing, in a test method that ends with a deliberate `asserterror Error(...)` to undo its own scratch changes, run under a `Disabled`- or `Codeunit`-isolated test runner. That rollback also erases the never-committed fixture; the next test still finds `IsInitialized = true` but the rows it depends on are gone. (Under a `Function`-isolated runner the fixture is lost regardless of `Commit()`, for the unrelated reason above — that is a runner-configuration problem, not this anti-pattern.) | ||
|
|
||
| See sample: `commit-shared-test-fixture-inside-lazy-initialize.bad.al`. | ||
|
|
||
| ## Source | ||
|
|
||
| The shared/lazy `Initialize()` pattern and its `Commit()` call are drawn from Luc van Vugt's "Let's talk about Shared Fixture and how to profit from this with the Dynamics NAV Test Toolkit": https://www.fluxxus.nl/index.php/bc/let39s-talk-about-shared-fixture-and-how-to-profit-from-this-with-the-dynamics-nav-test-toolkit/. That post shows the `Commit()` call in its `Initialize()` example but does not explain the transaction mechanics behind it; the `AutoCommit`-default, `Codeunit.Run`-error, and `TestIsolation`-level analysis above is this article's own, verified independently against Microsoft's TransactionModel/TestIsolation documentation and BCApps' `codeunit 134915 "ERM Online Mapping Setup"` source, not taken from the post. |
11 changes: 11 additions & 0 deletions
11
...ft/knowledge/testing/table-relation-test-exclude-known-invalid-relations-via-event.bad.al
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| codeunit 50141 "Sample Table Relation Test Ext" | ||
| { | ||
| [EventSubscriber(ObjectType::Codeunit, Codeunit::"Table Relation Test", 'OnAfterRemoveTableRelation', '', false, false)] | ||
| local procedure ExcludeSampleFieldFromTableRelationTest(var TableRelationsMetadata: Record "Table Relations Metadata" temporary) | ||
| var | ||
| TableRelationTest: Codeunit "Table Relation Test"; | ||
| begin | ||
| // Removes every relation on the whole table, not just the one known exception | ||
| TableRelationTest.RemoveTableRelation(TableRelationsMetadata, Database::"Sample Header", 0, 0, 0); | ||
| end; | ||
| } |
10 changes: 10 additions & 0 deletions
10
...t/knowledge/testing/table-relation-test-exclude-known-invalid-relations-via-event.good.al
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| codeunit 50141 "Sample Table Relation Test Ext" | ||
| { | ||
| [EventSubscriber(ObjectType::Codeunit, Codeunit::"Table Relation Test", 'OnAfterRemoveTableRelation', '', false, false)] | ||
| local procedure ExcludeSampleFieldFromTableRelationTest(var TableRelationsMetadata: Record "Table Relations Metadata" temporary) | ||
| var | ||
| TableRelationTest: Codeunit "Table Relation Test"; | ||
| begin | ||
| TableRelationTest.RemoveTableRelation(TableRelationsMetadata, Database::"Sample Header", 10, Database::"Sample Setup", 1); | ||
| end; | ||
| } |
30 changes: 30 additions & 0 deletions
30
...wledge/testing/table-relation-test-exclude-known-invalid-relations-via-event.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| --- | ||
| bc-version: [all] | ||
| domain: testing | ||
| keywords: [table-relation-test, tablerelationsmetadata, onafterremovetablerelation, field-length, field-type] | ||
| technologies: [al] | ||
| countries: [w1] | ||
| application-area: [all] | ||
| --- | ||
|
|
||
| # Exclude a known-valid TableRelation exception via OnAfterRemoveTableRelation | ||
|
|
||
| ## Description | ||
|
|
||
| Codeunit 134926 "Table Relation Test" (shipped in BCApps' test app — only consumers that depend on the BC test libraries can subscribe to it) reads Table Relations Metadata tenant-wide across every installed app, not just the current one, and fails the moment a related field's type or length doesn't match what the relation requires — the related field must match the largest related field's length, and its type must match (except a field may relate to both `Code` and `Text`, which resolves to `Text`). A field with a legitimate, intentional relation shape has no per-field override in its own object definition; the check runs with no built-in escape hatch. The validation test method itself is `[Scope('OnPrem')]`: it only runs from an on-premises test surface, not from a cloud-targeted test app, so this whole exception mechanism — and the check it works around — is only reachable where that test can actually execute. | ||
|
|
||
| ## Best Practice | ||
|
|
||
| Subscribe to `OnAfterRemoveTableRelation` and call the codeunit's own `RemoveTableRelation(TableRelationsMetadata, TableID, FieldID, RelatedTableID, RelatedFieldID)` to strike the one known-valid relation before the test evaluates it, scoped as narrowly as the exception actually is. Because the test itself is `[Scope('OnPrem')]`, do not recommend subscribing to it as a way to guard a cloud-targeted app's test suite — the subscription has no effect where the test never runs. | ||
|
|
||
| See sample: `table-relation-test-exclude-known-invalid-relations-via-event.good.al`. | ||
|
|
||
| ## Anti Pattern | ||
|
|
||
| Excluding an entire table's relations (or disabling the whole test codeunit) to work around one known exception. This discards the check's coverage for every other relation on that table, or in the app, not just the one that needed an exception. | ||
|
|
||
| See sample: `table-relation-test-exclude-known-invalid-relations-via-event.bad.al`. | ||
|
|
||
| ## Source | ||
|
|
||
| The `OnAfterRemoveTableRelation` exclusion technique is drawn from Luc van Vugt's "How-to: Test your Table Relations (2)": https://www.fluxxus.nl/index.php/bc/how-to-test-your-table-relations-2/. The codeunit/event signature, the `[Scope('OnPrem')]` boundary, and the tenant-wide `Table Relations Metadata` scope described above were verified directly against BCApps' `codeunit 134926 "Table Relation Test"` source, not taken from the post. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
18 changes: 18 additions & 0 deletions
18
microsoft/knowledge/testing/use-assert-isfalse-not-asserterror-for-boolean-checks.bad.al
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| codeunit 50143 "Sample Doc Amount Test" | ||
| { | ||
| Subtype = Test; | ||
|
|
||
| [Test] | ||
| procedure DocAmountIsNotVerifiedWhenLinesAreMissing() | ||
| var | ||
| Assert: Codeunit Assert; | ||
| PurchHeader: Record "Purchase Header"; | ||
| begin | ||
| asserterror Assert.IsTrue(VerifyDocAmount(PurchHeader), 'Doc. amount should not verify with no lines.'); | ||
| end; | ||
|
|
||
| local procedure VerifyDocAmount(var PurchHeader: Record "Purchase Header"): Boolean | ||
| begin | ||
| exit(false); | ||
| end; | ||
| } |
18 changes: 18 additions & 0 deletions
18
microsoft/knowledge/testing/use-assert-isfalse-not-asserterror-for-boolean-checks.good.al
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| codeunit 50143 "Sample Doc Amount Test" | ||
| { | ||
| Subtype = Test; | ||
|
|
||
| [Test] | ||
| procedure DocAmountIsNotVerifiedWhenLinesAreMissing() | ||
| var | ||
| Assert: Codeunit Assert; | ||
| PurchHeader: Record "Purchase Header"; | ||
| begin | ||
| Assert.IsFalse(VerifyDocAmount(PurchHeader), 'Doc. amount should not verify with no lines.'); | ||
| end; | ||
|
|
||
| local procedure VerifyDocAmount(var PurchHeader: Record "Purchase Header"): Boolean | ||
| begin | ||
| exit(false); | ||
| end; | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The length rule needs one qualification. Current
Table Relation Testrequires exact length when the metadata contains an unconditional relation. For conditional-only relations, it permits the source field to be longer than the largest related field and fails only when the source is shorter. Please document those two cases separately; otherwise agents will recommend exclusions or schema changes for conditional relations that the standard test accepts.