Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 Assert codeunit 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: Assert is 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’s app.json. Assert.AreEqual takes 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 the Rec/xRec bug 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 MarkInstallmentPaidSetsPaymentDate shouldn’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.IsTrue and Assert.AreEqual
  • Wrote a regression test for the xRec check from Chapter 9
  • Learned how to set a breakpoint and step through code

Tasks

  1. Create the test codeunit. Create Codeunits/LoanRepaymentMgtTests.Codeunit.al with Subtype = Test.
  2. Write the paid-date test. Add MarkInstallmentPaidSetsPaymentDate from 14.2.
  3. Write the regression test. Add MarkingPaidDoesNotTriggerBlockedEditError from 14.3, and confirm it passes against your current table.
  4. Break it on purpose. Temporarily change xRec.Paid to Paid in your table’s OnModify trigger, rerun the test, and confirm it now fails. Then put xRec.Paid back.
  5. Try it yourself. Write a third test for GenerateSchedule: given NumberOfInstallments = 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.