AL test development patterns with telemetry verification. MANDATORY when: (1) planning test implementation, (2) creating new tests, (3) modifying existing tests. Invoke proactively at ALL three stages. PLANNING: Structure plans with Red-Green-Refactor phases and use TodoWrite to track progress through each phase. CRITICAL RULE: DEBUG-* telemetry must be ZERO at both task START and END.
Applies test-driven development with telemetry verification to prove code paths during test creation.
/plugin marketplace add FBakkensen/bc-agentic-dev-tools/plugin install fbakkensen-tdd-plugins-tdd@FBakkensen/bc-agentic-dev-toolsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
references/NALICFTestTemplate.Codeunit.alWhen planning any test-related task, structure your plan with Red-Green-Refactor phases:
Phase 1 - Red (Design):
Phase 2 - Green (Implementation):
Phase 3 - Refactor (Cleanup):
When implementing tests, create todos for each phase:
1. [Red] Add DEBUG-TEST-START to <TestName>
2. [Red] Add DEBUG-BRANCH-* checkpoints to <ProductionCode>
3. [Green] Run al-build and verify test passes
4. [Green] Verify correct path in telemetry.jsonl
5. [Refactor] Remove DEBUG-* from production code
6. [Refactor] Remove DEBUG-TEST-START from test code
7. [Refactor] Verify zero DEBUG-* calls remain
This structure ensures every test task follows the complete verification cycle.
Expected starting state: Zero DEBUG-* telemetry calls in both production code (app/src/) and test code (test/src/).
Why: DEBUG telemetry is temporary scaffolding for proving code paths during development. It is NOT production instrumentation. If you find existing DEBUG-* calls, they indicate incomplete previous work.
Lifecycle:
If you find existing DEBUG- calls:*
They verify different things:
| Tool | Verifies | Question Answered |
|---|---|---|
| Assertions | Production code behavior | "Does the code produce correct output?" |
| DEBUG telemetry | Test setup correctness | "Did we exercise the intended code path?" |
The False Positive Problem:
Assertions can pass even when test setup is wrong. Example:
Assert.AreEqual(7, ActualQuantity)Solution: Production code telemetry proves which path ran:
DEBUG-TEST-START in test → identifies which test is runningDEBUG-* in production → proves which branch executedEvery test procedure you write MUST include:
FeatureTelemetry.LogUsage('DEBUG-TEST-START', 'Testing', '<ExactProcedureName>');
as the FIRST line after variable declarations. This is non-negotiable.
Telemetry verification is manual—do not add telemetry parsing/assertions to AL tests.
Every test procedure MUST begin with a telemetry checkpoint immediately after variable declarations:
[Test]
procedure GivenX_WhenY_ThenZ()
var
FeatureTelemetry: Codeunit "Feature Telemetry";
// other variables...
begin
// FIRST LINE - Always add this checkpoint
FeatureTelemetry.LogUsage('DEBUG-TEST-START', 'Testing', 'GivenX_WhenY_ThenZ');
// [SCENARIO] ...
// [GIVEN] ...
// [WHEN] ...
// [THEN] ...
end;
Production code telemetry (e.g., pricing calculations, rule evaluations) can emit from multiple tests. Without test-start markers:
With test-start markers in telemetry.jsonl:
{"eventId":"DEBUG-TEST-START","message":"GivenX_WhenY_ThenZ",...}
{"eventId":"DEBUG-PRICING-CALC","message":"Configuration method selected",...}
{"eventId":"DEBUG-COMPONENT-TOTAL","message":"Sum: 150.00",...}
Now you can grep for your test name and see all subsequent logs belong to that test.
Step A (REQUIRED): Add test-start checkpoint as FIRST line after declarations:
FeatureTelemetry.LogUsage('DEBUG-TEST-START', 'Testing', '<ExactProcedureName>');
Step B (REQUIRED): Add branch checkpoints in PRODUCTION code at decision points:
// In the function under test (production code)
if SalesLine.FindFirst() then begin
FeatureTelemetry.LogUsage('DEBUG-BRANCH-SALESLINE', 'FeatureName', 'Sales Line found');
// ... Sales Line path
end else begin
FeatureTelemetry.LogUsage('DEBUG-BRANCH-NOSALESLINE', 'FeatureName', 'Using Config Header');
// ... Config Header path
end;
Rules:
FeatureTelemetry.LogUsage() (not Session.LogMessage())Format() for non-text values in custom dimensionsal-build).output/TestResults/last.xml.output/TestResults/telemetry.jsonlUseful telemetry fields: eventId, message, testCodeunit, testProcedure, callStack, customDimensions
Once tests are verified and green, delete all DEBUG-* telemetry from both locations:
Production code:
DEBUG-PRICING-CALC, DEBUG-COMPONENT-TOTAL)Test code:
DEBUG-TEST-START)DEBUG-* calls added for verificationBoth must be cleaned up—leaving DEBUG telemetry in either location pollutes logs and signals incomplete work.
After running tests, correlate logs to specific tests:
# Find all logs from a specific test
Select-String -Path .output/TestResults/telemetry.jsonl -Pattern "GivenX_WhenY_ThenZ"
# Find test-start markers to see test execution order
Select-String -Path .output/TestResults/telemetry.jsonl -Pattern "DEBUG-TEST-START"
In telemetry.jsonl, logs appear in execution order. After a DEBUG-TEST-START entry, all subsequent logs belong to that test until the next DEBUG-TEST-START.
After running tests, telemetry.jsonl shows execution order:
Test 1: GivenSalesLineExists...
DEBUG-TEST-START → GivenSalesLineExists...
DEBUG-BRANCH-SALESLINE → Sales Line found ✓ Correct path
Test 2: GivenNoSalesLine...
DEBUG-TEST-START → GivenNoSalesLine...
DEBUG-BRANCH-NOSALESLINE → Using Config Header ✓ Correct path
If wrong telemetry appears, test setup is broken:
Test 1: GivenSalesLineExists... (expects SALESLINE branch)
DEBUG-TEST-START → GivenSalesLineExists...
DEBUG-BRANCH-NOSALESLINE → Using Config Header ✗ WRONG PATH!
This catches bugs that assertions cannot—the assertion might still pass if both paths produce the same value.
When tests fail because standard BC code isn't behaving as expected, we can't add DEBUG telemetry directly to BC code. Instead, we create temporary event subscribers that emit telemetry from BC's published events.
This pattern applies to any BC subsystem—pricing, posting, warehouse, manufacturing, etc. The pricing engine example below is just one application of a universal debugging technique.
Use the bc-w1-reference skill to find events in the BC subsystem you're debugging:
# Find integration events in a specific subsystem
rg -n "IntegrationEvent" "../_aldoc/bc-w1/BaseApp/Source/Base Application/Pricing/"
rg -n "IntegrationEvent" "../_aldoc/bc-w1/BaseApp/Source/Base Application/Sales/"
rg -n "IntegrationEvent" "../_aldoc/bc-w1/BaseApp/Source/Base Application/Purchases/"
# Find events by name pattern
rg -l "OnAfterPost" "../_aldoc/bc-w1/BaseApp/Source/Base Application/"
rg -l "OnBeforeValidate" "../_aldoc/bc-w1/BaseApp/Source/Base Application/"
Or use the Task tool with subagent_type=Explore to search the bc-w1 mirror:
Task tool:
subagent_type: Explore
prompt: "Search the BC W1 source mirror for integration events in [subsystem].
Find events that fire during [specific operation] that could help debug [issue]."
Find relevant BC events using bc-w1-reference skill (see above)
Create temporary debug subscriber codeunit in test folder:
codeunit 50XXX "NALICF Debug [Subsystem] Subsc"
{
Access = Internal;
[EventSubscriber(ObjectType::Codeunit, Codeunit::"[BC Codeunit]", '[EventName]', '', false, false)]
local procedure OnAfter[Event](var [Params])
var
FeatureTelemetry: Codeunit "Feature Telemetry";
begin
FeatureTelemetry.LogUsage('DEBUG-BC-[SUBSYSTEM]-[EVENT]', '[Area]',
StrSubstNo('[Description]: %1', [RelevantValue]));
end;
}
Select-String -Path .output/TestResults/telemetry.jsonl -Pattern "DEBUG-BC-"
This example shows debugging the Price Calculation - V16 codeunit, but the same approach works for any BC subsystem (posting codeunits, document management, inventory, etc.):
codeunit 50105 "NALICF Debug Price Subsc"
{
Access = Internal;
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Price Calculation - V16", 'OnAfterFindLines', '', false, false)]
local procedure OnAfterFindLines(var PriceListLine: Record "Price List Line"; AmountType: Enum "Price Amount Type"; var IsHandled: Boolean)
var
FeatureTelemetry: Codeunit "Feature Telemetry";
begin
FeatureTelemetry.LogUsage('DEBUG-BC-PRICING-FINDLINES', 'Pricing',
StrSubstNo('Found %1 lines, IsHandled=%2', PriceListLine.Count(), IsHandled));
end;
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Price Calculation - V16", 'OnAfterCalcBestAmount', '', false, false)]
local procedure OnAfterCalcBestAmount(var PriceListLine: Record "Price List Line")
var
FeatureTelemetry: Codeunit "Feature Telemetry";
begin
FeatureTelemetry.LogUsage('DEBUG-BC-PRICING-BESTAMOUNT', 'Pricing',
StrSubstNo('BestAmount: UnitPrice=%1, Status=%2', PriceListLine."Unit Price", PriceListLine.Status));
end;
}
This revealed the V16 pricing engine wasn't enabled—leading to the fix:
LibraryPriceCalculation.EnableExtendedPriceCalculation();
LibraryPriceCalculation.SetupDefaultHandler("Price Calculation Handler"::"Business Central (Version 16.0)");
The same pattern applies to any BC area:
| Subsystem | Example Events to Subscribe |
|---|---|
| Sales Posting | OnAfterPostSalesDoc, OnBeforePostSalesDoc in "Sales-Post" |
| Purchase Posting | OnAfterPostPurchaseDoc, OnBeforePostPurchaseDoc in "Purch.-Post" |
| Inventory | OnAfterPostItemJnlLine in "Item Jnl.-Post Line" |
| Warehouse | OnAfterCreateWhseJnlLine in "Whse. Jnl.-Register Line" |
| Manufacturing | OnAfterPostProdOrder in "Production Order-Post" |
Use bc-w1-reference to discover the specific events available in each subsystem.
DEBUG-BC-* prefix to distinguish from app telemetryDefault: Do NOT specify [TransactionModel] on test methods.
Microsoft's standard BC tests (40,000+ test methods) rely on the TestRunner's TestIsolation property rather than individual test attributes. Only ~3% of BC standard tests specify [TransactionModel].
How isolation works in BC:
| Level | Where Configured | Effect |
|---|---|---|
| TestRunner | TestIsolation property on test runner codeunit | Controls rollback for all tests run by that runner |
| Test Method | [TransactionModel] attribute | Overrides TestRunner for that specific test |
When to use [TransactionModel] (exceptions only):
| Attribute | Use When |
|---|---|
[TransactionModel(AutoRollback)] | Testing pure logic that MUST NOT call Commit(). Will ERROR if code under test commits. |
[TransactionModel(AutoCommit)] | Testing code that calls Commit() (posting routines, job queue, background sessions). Requires explicit cleanup. |
[TransactionModel(None)] | Simulating real user behavior where each page interaction is a separate transaction. Rare. |
Why NOT to default to AutoRollback:
Commit() (posting, background jobs)When creating new test codeunits, follow the structure in NALICFTestTemplate.Codeunit.al.
Key elements:
FeatureTelemetry and IsInitialized variables[SCENARIO], [GIVEN], [WHEN], [THEN]Initialize() procedure with IsInitialized guard[TransactionModel] by default — let TestRunner handle isolationNew test codeunit setup:
test/src/Workflows/<Feature>/NALICF<Feature>Test.Codeunit.alal-object-id-allocator skill[TransactionModel] if you have a specific reason (see table above)This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.