bcquality/microsoft/knowledge/testing/use-generateguid-for-unique-test-fixture-values.md
Michael Dieringer db9c0f275a Fix remaining correctness issues from Jesper's 2026-09-15 re-review
- use-generateguid-for-unique-test-fixture-values.md: narrowed the
  collision rule to primary-key/unique-lookup fields - an ordinary
  descriptive field carries no uniqueness constraint, so a hardcoded
  or deterministic value there isn't the anti-pattern (the article and
  its al-testing-review.md worklist cue both said "primary-key or
  descriptive field"). Also corrected GenerateRandomCode: it opens its
  target table as a temporary RecordRef that starts and stays empty,
  so its repeat/until loop always exits after one iteration and never
  retries even within a single test run - the "non-colliding within a
  test run" claim was false. It's the rightmost N characters of
  GenerateGUID()'s sequential series, so a short field's value cycles
  (Code[1] repeats every 10 calls, Code[2] every 100). Verified against
  LibraryUtility.Codeunit.al in the BCApps reference clone.
- item-ledger-entry-document-no-follows-last-shipping-no: both
  fixtures called FindSet() without consuming its optional Boolean,
  which raises a runtime error on an empty result set - the opposite
  of the article's own claimed "silently matches zero rows, no error"
  behavior. Wrapped in `if ... then;` per the existing
  guard-database-reads.good.al idiom.
- al-data-modeling-review.md: widened both not-applicable scope
  clauses (intro and outcome) to include dimension wiring, posting-
  routine structure, and Item Ledger Entry document-number lookups -
  the leaf declared itself not-applicable outside setup/master/key/
  numbering/block/audit surfaces despite having a targeted cue for
  this PR's own new article.
- Converted this PR's 8 plain-backtick "See sample: `x.good.al`."
  references (across all 4 new articles) to the READ-convention
  markdown-link form required by Knowledge-Retrieval.ps1.

Rebased onto upstream/main (one conflict in al-data-modeling-review.md
intro wording, merged).
2026-09-21 22:35:42 +02:00

4.1 KiB
Raw Blame History

bc-version domain keywords technologies countries application-area
all
testing
generateguid
library-utility
test-fixtures
uniqueness
generaterandomcode
maxstrlen
al
w1
all

Generate unique test fixture values with LibraryUtility helpers, not hardcoded literals

Description

A fixture helper that assigns a hardcoded literal to a primary-key field, or to any field the test relies on as a unique lookup identifier, collides the moment two tests, or two runs of the same test, create that fixture without cleanup — and a literal longer than the field allows raises a truncation or insert error. An ordinary descriptive field carries no such constraint: two rows with the same description do not collide on insert, and a deterministic descriptive value is often exactly what an exact-match assertion needs, so none of this applies to it. LibraryUtility.GenerateGUID() is not a real GUID — it is a Code[10] number-series value (GU00000000–GU99999999) — and it returns the full 10 characters unshortened. Truncating it yourself with CopyStr(..., 1, MaxStrLen(ShorterField)) for a field under 10 characters is unsafe: the changing digits sit at the right end and are exactly what gets cut off, so consecutive calls into a short field can produce the same truncated value. GenerateGUID() is only safe as-is for a field that holds the full 10 characters.

Best Practice

For a field that holds the full 10 characters, assign LibraryUtility.GenerateGUID() directly. For a shorter field, do not truncate a GUID yourself — but also do not assume every LibraryUtility helper verifies uniqueness against the real table, because they don't all behave the same way:

  • GenerateRandomCode(FieldNo, TableNo) opens the target table as a temporary RecordRef: the buffer starts and stays empty, so its repeat...until RecRef.IsEmpty() loop always exits after one iteration — despite taking TableNo, it never checks the real table, and it never retries even within its own call. Its value is the rightmost FieldRef.Length characters of GenerateGUID()'s sequential GU00000000–GU99999999 series, so for a short field that window of digits cycles: a 1-character field repeats every 10 calls, a 2-character field every 100, and so on. It is a finite short-field namespace with a low collision chance within one test run — not a guarantee at any scope, unlike the table-checking helpers below.
  • GenerateRandomCodeWithLength(FieldNo, TableNo, CodeLength) opens the real (non-temporary) table and loops until the generated value doesn't collide — a genuine verified-unique guarantee — but it returns Code[10] regardless of the requested CodeLength, so it's only useful for a field of 10 characters or fewer.
  • GenerateRandomCode20(FieldNo, TableNo) is the same real, verified-against-the-table pattern as GenerateRandomCodeWithLength, sized for a Code[20] field.
  • GenerateRandomXMLText(Length) performs no table lookup at all — it's a plain random-text generator, appropriate for a descriptive/incidental field where uniqueness doesn't matter, not for a value that needs to be collision-checked.

Pick GenerateRandomCodeWithLength/GenerateRandomCode20 when the test genuinely needs a code verified unique against the table; use GenerateRandomCode/GenerateGUID/GenerateRandomXMLText for incidental values where a low collision chance is enough.

See sample: use-generateguid-for-unique-test-fixture-values.good.al.

Anti Pattern

Hardcoding a primary-key or unique-lookup fixture value such as 'TEST001', which collides across parallel or repeated test runs — a fixed descriptive value is not this anti-pattern, since the field carries no uniqueness constraint. Equally an anti-pattern: truncating GenerateGUID()'s result with CopyStr(..., 1, MaxStrLen(Field)) for a field shorter than 10 characters — the truncation removes the part of the value that actually varies.

See sample: use-generateguid-for-unique-test-fixture-values.bad.al.