Chapter 14: Testing and Debugging
Objectives
By the end of this chapter you will be able to:
- Explain what a test codeunit is and how it differs from a normal one
- Write a test method using the AAA pattern (Arrange, Act, Assert)
- Use the
Assertcodeunit to verify expected results - Set a breakpoint and step through code with the VS Code debugger
- Apply testing best practices you can defend in a code review
14.1 What is a test codeunit?
A test codeunit is a normal codeunit with Subtype = Test that contains
methods marked [Test]. The test runner calls each [Test] method.
Each one either passes silently or fails with a message that points at
exactly what went wrong. You get that feedback far faster than by
clicking through the UI by hand after every change.
codeunit 50102 "Loan Repayment Mgt. Tests"
{
Subtype = Test;
[Test]
procedure MarkInstallmentPaidSetsPaymentDate()
begin
// test body goes here
end;
}
14.2 Arrange, Act, Assert
Every test method has the same three-part shape: set up the data you need (Arrange), call the code under test (Act), then check the result (Assert).
[Test]
procedure MarkInstallmentPaidSetsPaymentDate()
var
LoanRepaymentSchedule: Record "Loan Repayment Schedule";
LoanRepaymentMgt: Codeunit "Loan Repayment Mgt.";
Assert: Codeunit Assert;
begin
// Arrange
LoanRepaymentSchedule.Init();
LoanRepaymentSchedule."Loan No." := 'LOAN0001';
LoanRepaymentSchedule."Installment No." := 1;
LoanRepaymentSchedule."Principal Amount" := 1000;
LoanRepaymentSchedule."Interest Amount" := 50;
LoanRepaymentSchedule.Insert(true);
// Act
LoanRepaymentMgt.MarkInstallmentPaid(LoanRepaymentSchedule);
// Assert
Assert.IsTrue(LoanRepaymentSchedule.Paid, 'Installment should be marked paid.');
Assert.AreEqual(Today, LoanRepaymentSchedule."Payment Date", 'Payment date should be today.');
end;
Hint:
Assertis codeunit 130000 from Microsoft’s test libraries. Declare a variable for it like any other codeunit, and list the test libraries as dependencies in your test app’sapp.json.Assert.AreEqualtakes the expected value first and the actual value second. Getting this backwards doesn’t break the test, but it makes failure messages confusing to read later.
14.3 A regression test for the xRec check
A regression test is written specifically so that a fixed bug can never
silently come back. Chapter 9 showed that an OnModify trigger checking
Paid instead of xRec.Paid would block the transition to paid. That
rule is worth locking in with a test.
[Test]
procedure MarkingPaidDoesNotTriggerBlockedEditError()
var
LoanRepaymentSchedule: Record "Loan Repayment Schedule";
LoanRepaymentMgt: Codeunit "Loan Repayment Mgt.";
Assert: Codeunit Assert;
begin
// Arrange
LoanRepaymentSchedule.Init();
LoanRepaymentSchedule."Loan No." := 'LOAN0001';
LoanRepaymentSchedule."Installment No." := 1;
LoanRepaymentSchedule.Insert(true);
// Act — this should succeed, not raise the "cannot be changed" error
LoanRepaymentMgt.MarkInstallmentPaid(LoanRepaymentSchedule);
// Assert
Assert.IsTrue(LoanRepaymentSchedule.Paid, 'Marking paid should succeed without error.');
end;
Hint: If someone later changes the trigger to check
Paid, this test fails straight away and catches theRec/xRecbug before it ever reaches a user. Writing the test as soon as you fix a bug is what keeps the bug fixed.
14.4 Debugging with breakpoints
Not every problem is worth writing a test for first. Sometimes you just need to see what’s happening. In VS Code, click in the gutter to the left of a line number to set a breakpoint, then launch with debugging (F5). Execution pauses at that line. From there you can inspect variable values, step line by line (F10), or step into a called procedure (F11).
Hint: Set the breakpoint on the first line inside the loop or procedure you suspect, not somewhere further down. You want to catch the state as early as possible, before it has had a chance to go wrong.
14.5 Best practices for testing and debugging
- Keep to one assertion concept per test. A test named
MarkInstallmentPaidSetsPaymentDateshouldn’t also silently check three unrelated things. - Name tests after what they verify, not after the procedure they call. When a test fails later, its name tells you what was being checked.
- Write a regression test as soon as you fix a bug, not “later.”
- Reach for a breakpoint before you add
Message()calls everywhere. Messages linger in the code after you’re done debugging, and breakpoints don’t.
Chapter summary
In this chapter you:
- Wrote a test codeunit with
Subtype = Test - Structured a test with Arrange, Act, Assert
- Used
Assert.IsTrueandAssert.AreEqual - Wrote a regression test for the
xReccheck from Chapter 9 - Learned how to set a breakpoint and step through code
Tasks
- Create the test codeunit. Create
Codeunits/LoanRepaymentMgtTests.Codeunit.alwithSubtype = Test. - Write the paid-date test. Add
MarkInstallmentPaidSetsPaymentDatefrom 14.2. - Write the regression test. Add
MarkingPaidDoesNotTriggerBlockedEditErrorfrom 14.3, and confirm it passes against your current table. - Break it on purpose. Temporarily change
xRec.PaidtoPaidin your table’sOnModifytrigger, rerun the test, and confirm it now fails. Then putxRec.Paidback. - Try it yourself. Write a third test for
GenerateSchedule: givenNumberOfInstallments = 3, assert that exactly 3 records exist for the loan afterward.
Check your work: compare your test codeunit against the reference solution in the Chapter 14 solutions.