mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-10-05 14:46:55 +01:00
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.
This commit is contained in:
parent
07e324ddbc
commit
12b7f73d24
12 changed files with 218 additions and 0 deletions
|
|
@ -0,0 +1,19 @@
|
|||
codeunit 50142 "Sample Test Library"
|
||||
{
|
||||
var
|
||||
Initialized: Boolean;
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
codeunit 50142 "Sample Test Library"
|
||||
{
|
||||
var
|
||||
Initialized: Boolean;
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: testing
|
||||
keywords: [initialize, isinitialized, shared-fixture, commit, autorollback, 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 codeunit that creates master/setup data once, guarded by an `IsInitialized` flag, to avoid repeating expensive setup across many `[Test]` methods depends on that data surviving into every later test. Each `[Test]` method runs under `AutoRollback` by default, so data inserted during the first test's call to `Initialize()` rolls back at the end of that test. `IsInitialized` is a variable, not persisted data, so it still reads `true` on the next test — but the fixture rows it points to are already gone.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Call `Commit()` at the end of a lazy/shared `Initialize()` procedure, once the shared fixture data is created, so it survives past the first test's rollback boundary. Pair this with a `TestIsolation`-enabled test runner so the committed fixture is still cleaned up at the end of the full run.
|
||||
|
||||
See sample: `commit-shared-test-fixture-inside-lazy-initialize.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
A shared `Initialize()` guarded by `IsInitialized` that creates fixture records but never commits. The first test that runs it passes; every later test in the same codeunit either fails to find the fixture data or silently re-triggers setup logic that `IsInitialized` was meant to skip.
|
||||
|
||||
See sample: `commit-shared-test-fixture-inside-lazy-initialize.bad.al`.
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
codeunit 50140 "Sample Confirm Usage"
|
||||
{
|
||||
procedure ConfirmDeletion(RecordCount: Integer): Boolean
|
||||
var
|
||||
ConfirmMsg: Label 'Do you want to delete %1 records?';
|
||||
begin
|
||||
exit(Confirm(ConfirmMsg, false, RecordCount));
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
codeunit 50140 "Sample Confirm Usage"
|
||||
{
|
||||
procedure ConfirmDeletion(RecordCount: Integer): Boolean
|
||||
var
|
||||
ConfirmMsg: Label 'Do you want to delete %1 records?';
|
||||
begin
|
||||
exit(Confirm(StrSubstNo(ConfirmMsg, RecordCount), false));
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: testing
|
||||
keywords: [confirm, confirmhandler, strsubstno, placeholder, question, substitution]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Build the Confirm message with StrSubstNo, or a ConfirmHandler sees the raw template
|
||||
|
||||
## Description
|
||||
|
||||
`Confirm`'s placeholder-substitution overload — `Confirm('text %1', false, Value)` — substitutes the placeholder only for the dialog a real user sees. Inside a `[ConfirmHandler]`, the `Question` parameter received is the literal, unsubstituted template string (`'text %1'`), not the value-filled text. A test that asserts `Question` against the expected substituted message either fails outright or silently checks the wrong thing.
|
||||
|
||||
## Best Practice
|
||||
|
||||
When a `ConfirmHandler` needs to assert on the actual message text, build the string with `StrSubstNo(Text, Value)` in the production code first, and pass the already-substituted string to `Confirm()` with no further placeholder arguments.
|
||||
|
||||
See sample: `confirm-needs-strsubstno-before-confirmhandler-sees-substituted-text.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Calling `Confirm('text %1', false, Value)` and then asserting the substituted text against `Question` inside a `[ConfirmHandler]`. `Question` holds the raw `'text %1'` template, so the assertion never matches the intended message.
|
||||
|
||||
See sample: `confirm-needs-strsubstno-before-confirmhandler-sees-substituted-text.bad.al`.
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
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" walks every `TableRelation` field property in the app 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 across the whole app with no built-in escape hatch.
|
||||
|
||||
## 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.
|
||||
|
||||
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`.
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: testing
|
||||
keywords: [assert, isfalse, istrue, asserterror, boolean-check, negative-test]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Use Assert.IsFalse to check a boolean result, not asserterror around Assert.IsTrue
|
||||
|
||||
## Description
|
||||
|
||||
`asserterror` exists to assert that a statement raises a runtime error; it is not a general-purpose way to invert a boolean check. Wrapping `asserterror Assert.IsTrue(SomeFunc(), Msg)` to verify that `SomeFunc()` returns `false` tests whether `Assert.IsTrue`'s own error-raising behavior fired, not the value `SomeFunc()` actually returned.
|
||||
|
||||
## Best Practice
|
||||
|
||||
When the code under test returns a `Boolean` rather than raising an error, assert the value directly with `Assert.IsFalse(SomeFunc(), Msg)` (or `Assert.IsTrue` for the positive case). Reserve `asserterror` for statements expected to actually raise an error.
|
||||
|
||||
See sample: `use-assert-isfalse-not-asserterror-for-boolean-checks.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`asserterror Assert.IsTrue(SomeFunc(), Msg);` to verify `SomeFunc()` is `false`. It passes today because `Assert.IsTrue` happens to raise an error on failure, but it verifies the assertion helper's error-raising behavior, not the value under test.
|
||||
|
||||
See sample: `use-assert-isfalse-not-asserterror-for-boolean-checks.bad.al`.
|
||||
Loading…
Add table
Add a link
Reference in a new issue