Orphaned files meant for the original PR
This commit is contained in:
parent
9055fcbbd4
commit
6f72c21a60
2 changed files with 1943 additions and 0 deletions
1041
content/docs/agentic-coding/GettingMore/refactoring.md
Normal file
1041
content/docs/agentic-coding/GettingMore/refactoring.md
Normal file
File diff suppressed because it is too large
Load diff
902
content/docs/agentic-coding/GettingMore/testing.md
Normal file
902
content/docs/agentic-coding/GettingMore/testing.md
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue