Compare commits

..

18 commits

Author SHA1 Message Date
Jeremy Vyska
dbbc3d3cdc
Merge pull request #286 from microsoft/add-kine-knowledge-gap-article
Add article: AI for BC Development - The Knowledge Gap That Ships to Production
2026-04-01 11:54:55 +02:00
Jeremy Vyska
3e884878ab Add Kine article: AI Knowledge Gap That Ships to Production to vibe coding resources
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-04-01 09:19:07 +02:00
Jeremy Vyska
76159eb8cc
Merge pull request #283 from microsoft/agentic-assistance-hub
Orphaned files meant for the original PR
2026-01-20 14:24:42 +01:00
Jeremy Vyska
6f72c21a60 Orphaned files meant for the original PR 2026-01-20 14:23:48 +01:00
Jeremy Vyska
36c4ec1c0e
Merge pull request #275 from microsoft/issues/250
Fix #250: Clarify case sensitivity of APIPublisher and APIGroup
2025-12-16 12:24:39 +01:00
Henrik Helgesen
ed0868d8c1
Merge pull request #273 from microsoft/issues/150
Fix #150: Replace unavailable video with note
2025-12-03 12:44:37 -08:00
Henrik Helgesen
345e0f2a64
Merge pull request #274 from microsoft/issues/270
Fix #270: Add CAL Coding Guidelines PDF and fix broken link
2025-12-03 12:43:39 -08:00
Henrik Helgesen
b8d7ceccfc
Merge pull request #282 from javiarmesto/ai-native-dev-guideline
New Tool - AI Native AL Development toolkit
2025-12-03 12:37:25 -08:00
Javier Armesto Gonzalez
aa970ed0a6
Apply suggestion from @Copilot
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-28 12:00:55 +01:00
Javier Armesto Gonzalez
0042e10d5f
Add files via upload 2025-11-28 11:00:53 +01:00
Jeremy Vyska
d2cdf97e0c
Merge pull request #279 from microsoft/jeremy-siteops
Front page updates & fixes
2025-11-10 12:48:24 +01:00
Jeremy Vyska
3a262c4d33 Refresh of front page content 2025-11-10 12:47:46 +01:00
Jeremy Vyska
9ea56d5843 Fixing broken front page link 2025-11-10 12:39:14 +01:00
Jeremy Vyska
e508cb64fb
Merge pull request #278 from microsoft/main
Updating siteops branch for small fixes
2025-11-10 12:37:43 +01:00
Henrik Helgesen
afc35a8c51
Merge pull request #265 from igor-ctrl/patch-1
Update index.md
2025-10-23 09:50:39 -07:00
Jeremy Vyska
77b968ae1f Fix #270: Add CAL Coding Guidelines PDF and update broken link 2025-10-18 13:42:14 +02:00
Jeremy Vyska
8afc5570db Fix #150: Replace unavailable video with note about missing content 2025-10-18 13:35:27 +02:00
Igor
7dd5bae8c9
Update index.md
## Update No. Series pattern for BC v24+ modern API

Microsoft deprecated `NoSeriesManagement` in BC v24+. Added modern `codeunit "No. Series"` pattern documentation with simplified API examples, comparison table, and method reference. Legacy content preserved for backward compatibility.

Real-world implementation verified in production BC extension.
2025-10-01 15:34:12 -05:00
9 changed files with 2088 additions and 44 deletions

View file

@ -125,14 +125,14 @@ images: ["images/og-image-fission.png"]
<div class="card-shadow">
<span class="card-badge">NEW</span>
<h4 class="section-text-bold mt-4">
Vibe Coding for AL
Getting Started With Agentic Coding
</h4>
<div class="card-shadow-content">
<p class="section-text">
AI-optimized coding rules and guidelines designed to enhance the AL developer experience in modern AI-powered IDEs like VS Code and Cursor.
Essential concepts and practices for working with AI coding assistants in your AL development workflow.
</p>
<a href="/docs/vibe-coding/">
<button class="hero-mid-2 align-self-end">Explore Rules</button>
<a href="/docs/agentic-coding/gettingstarted/">
<button class="hero-mid-2 align-self-end">Get Started</button>
</a>
</div>
</div>
@ -140,16 +140,16 @@ images: ["images/og-image-fission.png"]
<div class="col-lg-4">
<div class="card-shadow">
<span class="card-badge">TAG</span>
<span class="card-badge">GUIDE</span>
<h4 class="section-text-bold mt-4">
API
Vibe Coding Rules
</h4>
<div class="card-shadow-content">
<p class="section-text">
Two API Related Design Patterns and one Best Practice for working with API's
AI-optimized coding rules and guidelines designed to enhance the AL developer experience in modern AI-powered IDEs.
</p>
<a href="/tags/api/"
><button class="hero-mid-2 align-self-end">Read Now</button></a
<a href="/docs/agentic-coding/vibe-coding-rules/"
><button class="hero-mid-2 align-self-end">Explore Rules</button></a
>
</div>
</div>
@ -157,17 +157,17 @@ images: ["images/og-image-fission.png"]
<div class="col-lg-4">
<div class="card-shadow">
<span class="card-badge">PATTERN</span>
<span class="card-badge">TOOLS</span>
<h4 class="section-text-bold mt-4">
Event Bridge
Agentic Tools
</h4>
<div class="card-shadow-content">
<p class="section-text">
In the world of interfaces, it is important to preserve (certain) events over multiple implementation of the interface.
Model Context Protocol (MCP) servers that enhance AI assistants for Business Central development workflows.
</p>
<a
href="/docs/patterns/event-bridge-pattern/"
><button class="hero-mid-2 align-self-end">Read Now</button></a
href="/docs/agentic-coding/communityresources/tools/"
><button class="hero-mid-2 align-self-end">Explore Tools</button></a
>
</div>
</div>
@ -210,17 +210,6 @@ images: ["images/og-image-fission.png"]
</p>
</div>
</a>
<a href="https://twitter.com/search?q=%23bcalhelp">
<div class="card-shadow twitter-card" style="margin-left: auto">
<div class="card-header-custom">
<h2 style="padding-right: 0.8rem;"><i class="fab fa-twitter"></i></h2>
<h2 class="section-text-bold">Twitter</h2>
</div>
<p class="section-text">
Chat about this with the <b class="text-highlight">#bcalhelp</b> hashtag.
</p>
</div>
</a>
</div>
</div>
</div>
@ -254,22 +243,6 @@ images: ["images/og-image-fission.png"]
</div></a
>
</div>
<div class="col-lg-6">
<a href="https://twitter.com/search?q=%23bcalhelp"
><div
class="card-shadow twitter-card-desktop"
style="margin-left: auto"
>
<div class="card-header-custom">
<h2 style="padding-right: 0.8rem;"><i class="fab fa-twitter"></i></h2>
<h2 class="section-text-bold">Twitter</h2>
</div>
<p class="section-text">
Chat about this with the <b class="text-highlight">#bcalhelp</b> hashtag.
</p>
</div></a
>
</div>
</div>
</div>
</div>

View file

@ -23,6 +23,6 @@ We're looking forward to your comments. Where you can, do provide concrete examp
[anchor0]: https://blogs.msdn.microsoft.com/nav/2015/01/09/cal-coding-guidelines-used-at-microsoft-development-center-copenhagen "download the C/AL coding guidelines as pdf"
[anchor0]: /files/CAL-Coding-Guidelines-at-Microsoft-Development-Center-Copenhagen.pdf "download the C/AL coding guidelines as pdf"
[anchor1]: /members/waldo/default.aspx "waldo"
[anchor2]: https://www.youtube.com/watch?v=z6skKy0pkmU&list=PLhZ3P-LY7CqmVszuvtJLujFyHpsVN0U_w&index=26

View file

@ -92,7 +92,9 @@ Entities where the Blocked Entity has been implemented include:
The [Released Entity][anchor4].
{{< youtube O2R-fTSup1o >}}
{{% alert title="Note" %}}
There was previously a video demonstration of this pattern, but it is no longer available.
{{% /alert %}}
[anchor0]: 2260.BlockedEntityPattern.png
[anchor1]: /navpatterns/1-patterns/blocked-entity/data-driven-blocked-entity/

View file

@ -0,0 +1,49 @@
---
title: "AL Development Collection"
linkTitle: "AL Development Collection"
weight: 1
description: >
AI Native AL Development toolkit with 37 Agent Primitives for systematic Business Central development
---
## Overview
The AL Development Collection provides a complete AI-native development toolkit for Microsoft Dynamics 365 Business Central. Instead of ad-hoc AI usage, you get systematic engineering through 37 Agent Primitives organized across 3 framework layers implementing the AI Native-Instructions Architecture.
**Created by**: [Javier Armesto](https://github.com/javiarmesto)
---
## Key Features
**6 Role-Based Specialist Agents**:
- al-architect 🏗️ (solution design), al-developer 💻 (implementation), al-debugger 🐛 (troubleshooting)
- al-tester ✅ (TDD/quality), al-api 🌐 (API development), al-copilot 🤖 (AI features)
**4 Orchestra System Agents** (Multi-agent TDD):
- al-conductor 🎭 (orchestration), al-planning-subagent 🔍 (research)
- al-implement-subagent 💻 (TDD implementation), al-review-subagent ✅ (validation)
**9 Auto-Applied Instructions**:
- Code style, naming conventions, performance patterns
- Error handling, events, testing standards
- Context-aware loading via `applyTo` patterns
**18 Agentic Workflows**:
- Environment setup (al-initialize, al-build)
- Development (al-events, al-pages, al-permissions)
- Analysis (al-diagnose, al-performance, al-migrate)
- Copilot features (al-copilot-capability, al-copilot-promptdialog, al-copilot-test)
**Smart Complexity Routing (Experimental)**:
- 🟢 LOW → al-developer (direct implementation)
- 🟡 MEDIUM → al-conductor (TDD orchestration)
- 🔴 HIGH → al-architect → al-conductor (full design + TDD)
---
## Links
- **GitHub**: https://github.com/javiarmesto/AL-Development-Collection-for-GitHub-Copilot
- **Quick Start**: https://github.com/javiarmesto/AL-Development-Collection-for-GitHub-Copilot/blob/main/QUICK-START.md
- **Complete Development Flow**: https://github.com/javiarmesto/AL-Development-Collection-for-GitHub-Copilot/blob/main/docs/workflows/complete-development-flow.md

View file

@ -14,6 +14,21 @@ This page curates blog posts, articles, and written guides from the Business Cen
### Getting Started
#### "AI for BC Development - The Knowledge Gap That Ships to Production" — Kine
**Author**: Kine (blog.kine.cz)
**Published**: 2025
**Summary**: A critical self-assessment guide on whether you are ready to validate what AI produces for BC development. Covers common AI mistakes in AL code (wrong field assignment, missing keys, reinventing standard libraries) and what BC knowledge areas you need to review AI output responsibly.
**Key Takeaways**:
- Common AI mistakes in AL code that ship to production
- Self-assessment framework for AI-assisted development readiness
- BC knowledge areas required to review AI output responsibly
- Practical examples of wrong field assignments, missing keys, and reinvented standard libraries
**Link**: https://blog.kine.cz/posts/bcdevelopmentserie-02b/
---
#### "Vibe Coding — yes or no?" — Demiliani
**Author**: Demiliani
**Published**: 2025-08-21

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,902 @@
---
title: "Writing Tests"
linkTitle: "Testing"
weight: 5
description: >
Use AI to create comprehensive test coverage for your AL code
---
## Scenario
You've developed new features for your Business Central extension, but you need comprehensive test coverage to:
- Ensure code works as expected
- Prevent regressions when making changes
- Document expected behavior
- Enable confident refactoring
- Meet quality standards
Writing tests manually is time-consuming, and you want to use AI to accelerate the process while maintaining test quality.
## Goal
Use AI to help you:
- Generate unit tests for individual procedures
- Create integration tests for complex workflows
- Design test data and scenarios
- Write test helpers and fixtures
- Create mock objects for dependencies
- Implement data-driven tests
## The Code to Test
Here's a codeunit that needs test coverage:
```al
codeunit 50100 "Order Discount Manager"
{
procedure CalculateDiscount(var SalesHeader: Record "Sales Header"): Decimal
var
Customer: Record Customer;
DiscountPct: Decimal;
begin
if not Customer.Get(SalesHeader."Sell-to Customer No.") then
Error('Customer %1 not found', SalesHeader."Sell-to Customer No.");
DiscountPct := GetCustomerDiscount(Customer);
DiscountPct += GetVolumeDiscount(SalesHeader);
DiscountPct += GetSeasonalDiscount();
if DiscountPct > 50 then
DiscountPct := 50;
exit(DiscountPct);
end;
local procedure GetCustomerDiscount(Customer: Record Customer): Decimal
begin
case Customer."Customer Discount Group" of
'VIP':
exit(10);
'PREMIUM':
exit(5);
else
exit(0);
end;
end;
local procedure GetVolumeDiscount(SalesHeader: Record "Sales Header"): Decimal
var
SalesLine: Record "Sales Line";
TotalAmount: Decimal;
begin
SalesLine.SetRange("Document Type", SalesHeader."Document Type");
SalesLine.SetRange("Document No.", SalesHeader."No.");
SalesLine.CalcSums("Line Amount");
TotalAmount := SalesLine."Line Amount";
case true of
TotalAmount >= 10000:
exit(15);
TotalAmount >= 5000:
exit(10);
TotalAmount >= 1000:
exit(5);
else
exit(0);
end;
end;
local procedure GetSeasonalDiscount(): Decimal
begin
if Date2DMY(Today, 2) in [11, 12] then
exit(5);
exit(0);
end;
}
```
## Step-by-Step Testing Process
### Step 1: Generate Basic Test Structure
**Prompt**:
```
Create a test codeunit for the Order Discount Manager.
Include:
- Proper test codeunit structure with Subtype = Test
- Setup and teardown if needed
- Test procedure stubs for all public procedures
- Use AL test framework conventions
- Include Assert codeunit
- Add test isolation attributes where needed
[Include the codeunit code]
```
**Expected Result**:
```al
codeunit 50101 "Order Discount Mgr. Tests"
{
Subtype = Test;
var
Assert: Codeunit "Library Assert";
LibrarySales: Codeunit "Library - Sales";
LibraryUtility: Codeunit "Library - Utility";
IsInitialized: Boolean;
[Test]
procedure TestCalculateDiscountBasic()
begin
// Test basic discount calculation
end;
[Test]
procedure TestVIPCustomerDiscount()
begin
// Test VIP customer gets 10% discount
end;
[Test]
procedure TestVolumeDiscountTiers()
begin
// Test volume discount calculation at different amounts
end;
[Test]
procedure TestSeasonalDiscount()
begin
// Test seasonal discount in November/December
end;
[Test]
procedure TestMaximumDiscountCap()
begin
// Test discount cannot exceed 50%
end;
[Test]
procedure TestCustomerNotFoundError()
begin
// Test error when customer doesn't exist
end;
local procedure Initialize()
begin
if IsInitialized then
exit;
// Setup code here
IsInitialized := true;
end;
}
```
### Step 2: Implement Happy Path Test
**Prompt**:
```
Implement the TestCalculateDiscountBasic test using the Given-When-Then pattern.
This test should verify:
- A standard customer with a medium-size order
- Gets the expected discount percentage
- No errors are raised
Use the Library codeunits for test data creation.
Include clear comments for each section.
```
**Expected Result**:
```al
[Test]
procedure TestCalculateDiscountBasic()
var
Customer: Record Customer;
SalesHeader: Record "Sales Header";
OrderDiscountMgr: Codeunit "Order Discount Manager";
ActualDiscount: Decimal;
ExpectedDiscount: Decimal;
begin
// [SCENARIO] Calculate discount for a standard order
Initialize();
// [GIVEN] A standard customer with no special discount group
Customer.Get(LibrarySales.CreateCustomerNo());
Customer."Customer Discount Group" := '';
Customer.Modify();
// [GIVEN] A sales order with total amount of 2000 (5% volume discount)
SalesHeader := CreateSalesOrderWithAmount(Customer."No.", 2000);
// [GIVEN] Expected discount is 5% (volume only, not in seasonal months)
ExpectedDiscount := 5;
// [WHEN] Calculating the discount
ActualDiscount := OrderDiscountMgr.CalculateDiscount(SalesHeader);
// [THEN] The discount matches expected value
Assert.AreEqual(ExpectedDiscount, ActualDiscount, 'Discount calculation incorrect');
end;
local procedure CreateSalesOrderWithAmount(CustomerNo: Code[20]; TotalAmount: Decimal): Record "Sales Header"
var
SalesHeader: Record "Sales Header";
SalesLine: Record "Sales Line";
Item: Record Item;
begin
LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Order, CustomerNo);
Item."No." := LibraryUtility.GenerateRandomCode(Item.FieldNo("No."), Database::Item);
Item."Unit Price" := TotalAmount;
Item.Insert();
LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::Item, Item."No.", 1);
SalesLine.Validate("Unit Price", TotalAmount);
SalesLine.Modify(true);
exit(SalesHeader);
end;
```
### Step 3: Implement Error Case Tests
**Prompt**:
```
Implement the TestCustomerNotFoundError test.
This should verify:
- When an invalid customer number is used
- An appropriate error is raised
- The error message contains the customer number
Use asserterror and Assert.ExpectedError.
```
**Expected Result**:
```al
[Test]
procedure TestCustomerNotFoundError()
var
SalesHeader: Record "Sales Header";
OrderDiscountMgr: Codeunit "Order Discount Manager";
InvalidCustomerNo: Code[20];
begin
// [SCENARIO] Error is raised when customer doesn't exist
Initialize();
// [GIVEN] A sales header with non-existent customer
InvalidCustomerNo := 'INVALID123';
LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Order, '');
SalesHeader."Sell-to Customer No." := InvalidCustomerNo;
SalesHeader.Modify();
// [WHEN] Calculating discount for non-existent customer
asserterror OrderDiscountMgr.CalculateDiscount(SalesHeader);
// [THEN] Appropriate error is raised with customer number
Assert.ExpectedError(StrSubstNo('Customer %1 not found', InvalidCustomerNo));
end;
```
### Step 4: Implement Data-Driven Tests
**Prompt**:
```
Create a comprehensive test for all customer discount tiers (VIP, PREMIUM, standard).
Use a data-driven approach where one test function tests all scenarios.
Create a local helper that takes customer group and expected discount as parameters.
Test these scenarios:
- VIP customer: 10% discount
- PREMIUM customer: 5% discount
- Standard customer: 0% discount
- Empty group: 0% discount
```
**Expected Result**:
```al
[Test]
procedure TestCustomerDiscountTiers()
begin
// [SCENARIO] Different customer types receive appropriate discounts
Initialize();
// [GIVEN] [WHEN] [THEN] VIP customers get 10% discount
VerifyCustomerTypeDiscount('VIP', 10);
// [GIVEN] [WHEN] [THEN] Premium customers get 5% discount
VerifyCustomerTypeDiscount('PREMIUM', 5);
// [GIVEN] [WHEN] [THEN] Standard customers get 0% discount
VerifyCustomerTypeDiscount('STANDARD', 0);
// [GIVEN] [WHEN] [THEN] Customers with no group get 0% discount
VerifyCustomerTypeDiscount('', 0);
end;
local procedure VerifyCustomerTypeDiscount(CustomerGroup: Code[10]; ExpectedDiscount: Decimal)
var
Customer: Record Customer;
SalesHeader: Record "Sales Header";
OrderDiscountMgr: Codeunit "Order Discount Manager";
ActualDiscount: Decimal;
begin
// Create customer with specific group
Customer.Get(LibrarySales.CreateCustomerNo());
Customer."Customer Discount Group" := CustomerGroup;
Customer.Modify();
// Create order with amount below volume discount threshold (< 1000)
SalesHeader := CreateSalesOrderWithAmount(Customer."No.", 500);
// Calculate discount
ActualDiscount := OrderDiscountMgr.CalculateDiscount(SalesHeader);
// Verify expected discount
Assert.AreEqual(ExpectedDiscount, ActualDiscount,
StrSubstNo('Incorrect discount for customer group %1', CustomerGroup));
end;
```
### Step 5: Test Edge Cases and Boundaries
**Prompt**:
```
Create tests for volume discount boundary conditions.
Test these specific amounts:
- 999: Just below 1000 threshold (expect 0% volume discount)
- 1000: Exactly at threshold (expect 5% volume discount)
- 4999: Just below 5000 threshold (expect 5% volume discount)
- 5000: Exactly at threshold (expect 10% volume discount)
- 9999: Just below 10000 threshold (expect 10% volume discount)
- 10000: Exactly at threshold (expect 15% volume discount)
Use descriptive test names and clear assertions.
```
**Expected Result**:
```al
[Test]
procedure TestVolumeDiscountBoundaries()
begin
// [SCENARIO] Volume discounts applied at exact thresholds
Initialize();
// Just below first threshold
VerifyVolumeDiscount(999, 0, 'Below 1000 threshold');
// Exactly at first threshold
VerifyVolumeDiscount(1000, 5, 'At 1000 threshold');
// Between first and second threshold
VerifyVolumeDiscount(4999, 5, 'Below 5000 threshold');
// Exactly at second threshold
VerifyVolumeDiscount(5000, 10, 'At 5000 threshold');
// Between second and third threshold
VerifyVolumeDiscount(9999, 10, 'Below 10000 threshold');
// Exactly at third threshold
VerifyVolumeDiscount(10000, 15, 'At 10000 threshold');
end;
local procedure VerifyVolumeDiscount(OrderAmount: Decimal; ExpectedVolumeDiscount: Decimal; Scenario: Text)
var
Customer: Record Customer;
SalesHeader: Record "Sales Header";
OrderDiscountMgr: Codeunit "Order Discount Manager";
ActualDiscount: Decimal;
begin
// Create standard customer (no customer discount)
Customer.Get(LibrarySales.CreateCustomerNo());
Customer."Customer Discount Group" := '';
Customer.Modify();
// Create order with specific amount
SalesHeader := CreateSalesOrderWithAmount(Customer."No.", OrderAmount);
// Calculate discount (assuming not in seasonal months)
ActualDiscount := OrderDiscountMgr.CalculateDiscount(SalesHeader);
// Verify volume discount
Assert.AreEqual(ExpectedVolumeDiscount, ActualDiscount,
StrSubstNo('Incorrect volume discount for %1 (amount: %2)', Scenario, OrderAmount));
end;
```
### Step 6: Test Maximum Discount Cap
**Prompt**:
```
Create a test that verifies the 50% maximum discount cap.
Test scenario:
- VIP customer (10% discount)
- Large order (15% volume discount)
- Seasonal period (5% discount)
- Total would be 30%, which is under cap - verify this works
- Then test a scenario that would exceed 50% and verify it's capped
Consider: What combination would exceed 50%? We may need to modify setup or mock GetSeasonalDiscount.
```
**Expected Result**:
```al
[Test]
procedure TestMaximumDiscountCap()
var
Customer: Record Customer;
SalesHeader: Record "Sales Header";
OrderDiscountMgr: Codeunit "Order Discount Manager";
ActualDiscount: Decimal;
begin
// [SCENARIO] Discount is capped at 50% even when combined discounts exceed it
Initialize();
// [GIVEN] VIP customer (10% discount)
Customer.Get(LibrarySales.CreateCustomerNo());
Customer."Customer Discount Group" := 'VIP';
Customer.Modify();
// [GIVEN] Large order (15% volume discount)
SalesHeader := CreateSalesOrderWithAmount(Customer."No.", 10000);
// [GIVEN] If testing in Nov/Dec, total would be 10+15+5=30% (under cap)
// [GIVEN] To test cap, we need a scenario that would exceed 50%
// Note: With current business logic, max is 30%, so cap isn't reached
// [WHEN] Calculating discount
ActualDiscount := OrderDiscountMgr.CalculateDiscount(SalesHeader);
// [THEN] Discount doesn't exceed 50%
Assert.IsTrue(ActualDiscount <= 50, 'Discount exceeds maximum allowed 50%');
end;
[Test]
procedure TestDiscountCapWithMultipleDiscounts()
var
Customer: Record Customer;
SalesHeader: Record "Sales Header";
OrderDiscountMgr: Codeunit "Order Discount Manager";
ActualDiscount: Decimal;
begin
// [SCENARIO] Combined discounts that would exceed 50% are capped
// [NOTE] This test may need the business logic modified to make it relevant
// Current max possible: 10% (VIP) + 15% (volume) + 5% (seasonal) = 30%
Initialize();
// Create scenario with maximum possible discounts
Customer.Get(LibrarySales.CreateCustomerNo());
Customer."Customer Discount Group" := 'VIP';
Customer.Modify();
SalesHeader := CreateSalesOrderWithAmount(Customer."No.", 10000);
// Calculate discount
ActualDiscount := OrderDiscountMgr.CalculateDiscount(SalesHeader);
// Verify cap is applied if needed
Assert.IsTrue(ActualDiscount <= 50, 'Discount cap not enforced');
end;
```
### Step 7: Test Time-Dependent Logic
**Prompt**:
```
Create tests for the seasonal discount that depends on the current date.
Since GetSeasonalDiscount uses Today, we need to:
1. Test behavior for November (month 11)
2. Test behavior for December (month 12)
3. Test behavior for other months
Explain how to handle date-dependent tests in AL.
Options:
- Make the date a parameter (refactor code)
- Use test isolation / mocking
- Create tests that work year-round
```
**Expected Result**:
```al
[Test]
procedure TestSeasonalDiscountInNovember()
var
Customer: Record Customer;
SalesHeader: Record "Sales Header";
OrderDiscountMgr: Codeunit "Order Discount Manager";
ActualDiscount: Decimal;
CurrentMonth: Integer;
begin
// [SCENARIO] Seasonal discount is applied in November
// [NOTE] This test only passes when run in November
Initialize();
CurrentMonth := Date2DMY(Today, 2);
if CurrentMonth <> 11 then
exit; // Skip test if not November
// [GIVEN] Standard customer with small order (no other discounts)
Customer.Get(LibrarySales.CreateCustomerNo());
Customer."Customer Discount Group" := '';
Customer.Modify();
SalesHeader := CreateSalesOrderWithAmount(Customer."No.", 500);
// [WHEN] Calculating discount in November
ActualDiscount := OrderDiscountMgr.CalculateDiscount(SalesHeader);
// [THEN] 5% seasonal discount is applied
Assert.AreEqual(5, ActualDiscount, 'Seasonal discount not applied in November');
end;
[Test]
procedure TestNoSeasonalDiscountInJanuary()
var
Customer: Record Customer;
SalesHeader: Record "Sales Header";
OrderDiscountMgr: Codeunit "Order Discount Manager";
ActualDiscount: Decimal;
CurrentMonth: Integer;
begin
// [SCENARIO] No seasonal discount in non-holiday months
Initialize();
CurrentMonth := Date2DMY(Today, 2);
if CurrentMonth in [11, 12] then
exit; // Skip test if in seasonal period
// [GIVEN] Standard customer with small order
Customer.Get(LibrarySales.CreateCustomerNo());
Customer."Customer Discount Group" := '';
Customer.Modify();
SalesHeader := CreateSalesOrderWithAmount(Customer."No.", 500);
// [WHEN] Calculating discount outside seasonal period
ActualDiscount := OrderDiscountMgr.CalculateDiscount(SalesHeader);
// [THEN] No discount is applied
Assert.AreEqual(0, ActualDiscount, 'Unexpected discount outside seasonal period');
end;
// [RECOMMENDATION] Refactor GetSeasonalDiscount to accept date parameter
// This would make testing much easier and more reliable:
// local procedure GetSeasonalDiscount(CheckDate: Date): Decimal
```
### Step 8: Integration Test
**Prompt**:
```
Create an integration test that tests the complete workflow:
1. Create a new customer with VIP status
2. Create a sales order for that customer
3. Add multiple lines totaling over 10,000
4. Calculate the discount
5. Verify all discount types are correctly combined
6. Verify the discount is applied to the sales header
This test should verify the codeunit works correctly with real BC data and tables.
```
**Expected Result**:
```al
[Test]
procedure TestCompleteDiscountWorkflow()
var
Customer: Record Customer;
SalesHeader: Record "Sales Header";
SalesLine: Record "Sales Line";
Item: Record Item;
OrderDiscountMgr: Codeunit "Order Discount Manager";
ExpectedDiscount: Decimal;
ActualDiscount: Decimal;
CurrentMonth: Integer;
begin
// [SCENARIO] Complete workflow of discount calculation and application
Initialize();
// [GIVEN] A VIP customer
Customer.Get(LibrarySales.CreateCustomerNo());
Customer."Customer Discount Group" := 'VIP';
Customer.Modify();
// [GIVEN] A sales order with multiple lines totaling 12,000
LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Order, Customer."No.");
// Add three items
CreateItemAndSalesLine(SalesHeader, 5000);
CreateItemAndSalesLine(SalesHeader, 4000);
CreateItemAndSalesLine(SalesHeader, 3000);
// [GIVEN] Expected discount calculation:
// - VIP customer: 10%
// - Volume (>10000): 15%
// - Seasonal (if Nov/Dec): 5%
CurrentMonth := Date2DMY(Today, 2);
if CurrentMonth in [11, 12] then
ExpectedDiscount := 30 // 10 + 15 + 5
else
ExpectedDiscount := 25; // 10 + 15
// [WHEN] Calculating the discount
ActualDiscount := OrderDiscountMgr.CalculateDiscount(SalesHeader);
// [THEN] The correct combined discount is calculated
Assert.AreEqual(ExpectedDiscount, ActualDiscount,
'Complete discount calculation incorrect');
// [THEN] Verify order total is correct
SalesLine.SetRange("Document Type", SalesHeader."Document Type");
SalesLine.SetRange("Document No.", SalesHeader."No.");
SalesLine.CalcSums("Line Amount");
Assert.AreEqual(12000, SalesLine."Line Amount", 'Order total incorrect');
end;
local procedure CreateItemAndSalesLine(SalesHeader: Record "Sales Header"; Amount: Decimal)
var
Item: Record Item;
SalesLine: Record "Sales Line";
begin
Item."No." := LibraryUtility.GenerateRandomCode(Item.FieldNo("No."), Database::Item);
Item."Unit Price" := Amount;
Item.Insert();
LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::Item, Item."No.", 1);
SalesLine.Validate("Unit Price", Amount);
SalesLine.Modify(true);
end;
```
## Review Points: What to Check in Tests
### ✅ Test Quality
- [ ] Tests are independent (can run in any order)
- [ ] Tests are repeatable (same result every time)
- [ ] Tests have clear Given-When-Then structure
- [ ] Test names clearly describe what is being tested
- [ ] Assertions have descriptive error messages
### ✅ Test Coverage
- [ ] Happy path scenarios covered
- [ ] Error cases tested
- [ ] Edge cases and boundaries tested
- [ ] All public procedures have tests
- [ ] Integration scenarios tested
### ✅ Test Maintainability
- [ ] Helper methods reduce duplication
- [ ] Test data creation is centralized
- [ ] Tests are easy to understand
- [ ] Tests don't depend on specific data
- [ ] Setup and teardown properly implemented
### ✅ Test Performance
- [ ] Tests run quickly
- [ ] Minimal database operations
- [ ] Proper use of test isolation
- [ ] No unnecessary waits or delays
## Advanced Testing Patterns
### Pattern 1: Test Fixtures
**Prompt**:
```
Create a test fixture class for sales orders that provides:
- Standard order (customer with no discounts, small amount)
- VIP order (VIP customer, medium amount)
- Large order (standard customer, large amount)
- Complex order (VIP customer, large amount, multiple lines)
These fixtures should be reusable across all tests.
```
### Pattern 2: Mock Objects
**Prompt**:
```
The GetSeasonalDiscount procedure is hard to test because it depends on Today.
Refactor the code to use dependency injection:
1. Create an interface for date provider
2. Inject the date provider into the codeunit
3. Create a mock date provider for testing
4. Show how to test with different dates
```
### Pattern 3: Test Data Builders
**Prompt**:
```
Create a fluent test data builder for sales orders:
SalesOrderBuilder
.WithCustomer(CustomerNo)
.WithVIPStatus()
.WithLine(ItemNo, Quantity, Price)
.WithTotalAmount(Amount)
.Build()
This makes test data creation more readable and flexible.
```
## Best Practices for AI-Assisted Testing
### 1. Start with Test Cases, Then Generate
```
Before generating test code, help me identify all test cases for this procedure:
- What scenarios should be tested?
- What are the edge cases?
- What error conditions exist?
- What are the boundary conditions?
[Include procedure code]
```
### 2. Generate Tests in Batches
```
Generate tests for these three related procedures together so they share test helpers:
- CalculateDiscount
- ApplyDiscount
- ValidateDiscount
```
### 3. Request Explanatory Comments
```
Generate the test with detailed comments explaining:
- Why this test case is important
- What could go wrong if this test didn't exist
- Any gotchas or special considerations
```
### 4. Ask for Test Improvements
```
Review this test I wrote. Suggest improvements for:
- Clarity and readability
- Better assertions
- Edge cases I might have missed
- Ways to make it more maintainable
```
### 5. Generate Test Documentation
```
Create documentation for this test suite explaining:
- What is being tested
- Test coverage summary
- How to run the tests
- How to add new tests
- Known limitations
```
## Common Testing Challenges
### Challenge 1: Testing Private Methods
**Problem**: Local procedures can't be tested directly
**Solution**:
```
I need to test this local procedure. Options:
1. Make it public (if appropriate)
2. Test it indirectly through public procedures
3. Extract to a separate testable codeunit
Which approach is best for this scenario? [Include code]
```
### Challenge 2: Testing Database Operations
**Problem**: Tests that modify database are slow and fragile
**Solution**:
```
This codeunit performs database operations. Help me:
1. Identify which operations need real database
2. Which can be mocked or isolated
3. Create a testing strategy that balances coverage and speed
```
### Challenge 3: Testing External Dependencies
**Problem**: Code calls external services or APIs
**Solution**:
```
This code calls an external API. Create:
1. An interface for the API
2. A mock implementation for testing
3. Tests using the mock
4. Integration tests for the real API (marked for manual runs)
```
## Practice Exercise
Write comprehensive tests for this codeunit:
```al
codeunit 50200 "Credit Limit Checker"
{
procedure CheckCreditLimit(CustomerNo: Code[20]; NewOrderAmount: Decimal): Boolean
var
Customer: Record Customer;
CustLedgerEntry: Record "Cust. Ledger Entry";
TotalOutstanding: Decimal;
begin
if not Customer.Get(CustomerNo) then
Error('Customer not found');
Customer.CalcFields("Balance (LCY)");
TotalOutstanding := Customer."Balance (LCY)" + NewOrderAmount;
if Customer."Credit Limit (LCY)" = 0 then
exit(true);
exit(TotalOutstanding <= Customer."Credit Limit (LCY)");
end;
}
```
**Your Tasks**:
1. List all test scenarios
2. Create test codeunit structure
3. Implement tests for:
- Customer not found
- No credit limit (unlimited)
- Within credit limit
- Exactly at credit limit
- Over credit limit
- Edge cases
4. Add integration test
5. Review and improve tests
## Next Steps
- Learn how [code review](../code-review) can verify test quality
- See how [refactoring](../refactoring) benefits from good tests
- Explore [documentation](../documentation) for test procedures

View file

@ -10,6 +10,68 @@ _Created by Microsoft, Described by Jeremy Vyska (Spare Brained Ideas)_
The "Number Series" system is used extensively to provide numbers to master records, documents, and other transactions through Microsoft Dynamics 365 Business Central.
## Important: BC v24+ Modern Pattern (Updated 2024)
**As of Business Central version 24.0 and later**, Microsoft deprecated the `NoSeriesManagement` codeunit in favor of the new `codeunit "No. Series"` with simplified methods.
### Modern Implementation (BC v24+)
**Variable declaration:**
```al
var
NoSeries: Codeunit "No. Series";
```
**OnInsert pattern (simplified):**
```al
trigger OnInsert()
begin
if "No." = '' then begin
MySetup.Get();
MySetup.TestField("Document Nos.");
"No. Series" := MySetup."Document Nos.";
if NoSeries.AreRelated(MySetup."Document Nos.", xRec."No. Series") then
"No. Series" := xRec."No. Series";
"No." := NoSeries.GetNextNo("No. Series");
end;
end;
```
**OnValidate pattern (same as before):**
```al
trigger OnValidate()
begin
if "No." <> xRec."No." then begin
MySetup.Get();
NoSeries.TestManual(MySetup."Document Nos.");
"No. Series" := '';
end;
end;
```
### Key Differences from Legacy Pattern
| Legacy (NoSeriesManagement) | Modern (No. Series) |
|----------------------------|---------------------|
| `NoSeriesMgt.InitSeries(...)` - 5 parameters | `NoSeries.GetNextNo(...)` - 1-2 parameters |
| `NoSeriesMgt.TestManual(...)` | `NoSeries.TestManual(...)` - Same method name |
| `NoSeriesMgt.TryGetNextNo(...)` | `NoSeries.PeekNextNo(...)` - New name |
| `NoSeriesMgt.SelectSeries(...)` | `NoSeries.AreRelated(...)` - Simplified API |
| Complex parameter passing | Simplified, intuitive API |
### Migration Strategy
For **backward compatibility** (supporting both BC v23 and v24+):
1. Use conditional compilation with `#if` directives based on platform version
2. Check runtime platform version and branch logic accordingly
3. Implement both patterns in separate procedures with version detection
{{% alert title="Note" color="warning" %}}
**The examples below reflect the LEGACY pattern** (NoSeriesManagement codeunit) for reference and historical context. For new development on BC v24+, use the modern `codeunit "No. Series"` pattern shown above.
{{% /alert %}}
---
## Description
At the heart of things, the Number Series engine allows users to define structure for a sequential numeric or alphanumeric string (collectively referred to as a 'number series'), then assign that structure to different parts of the system.
@ -283,4 +345,4 @@ For usage of number series, there is more information available on:
- [Microsoft Docs: Create Number Series](https://docs.microsoft.com/en-us/dynamics365/business-central/ui-create-number-series)
- [Microsoft Learn: Set up number series and trail codes](https://docs.microsoft.com/en-us/learn/modules/number-series-trail-codes-dynamics-365-business-central/)
For more programming details, there is more information on [Microsoft Docs: Number Sequences in Business Central](https://docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-number-sequences).
For more programming details, there is more information on [Microsoft Docs: Number Sequences in Business Central](https://docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-number-sequences).