Getting Started with Testing in Business Central

So you’ve built some cool functionality in Business Central, but how do you make sure it keeps working as you add more features? That’s where testing comes in. Let me walk you through the basics with some real examples that actually work.

If you are building a production extension (not a test extension) and you only need the core System Application (which is almost always the case), you add just this dependency:

In Business Central, tests are just special codeunits with some extra attributes. Here’s a practical example that tests a document shipping type table:

codeunit 51050 "Basic Tests"
{
    Subtype = Test;
    TestPermissions = Disabled;

    [Test]
    [TransactionModel(TransactionModel::AutoRollback)]
    procedure InsertNewDocumentType_Succeeds()
    var
        DocType: Record "Type of Document Shipping";
    begin
        // Simple test: can we create and retrieve a record?
        DocType.Init();
        DocType."Type of Legal Doc. Shipping" := 'SS';
        DocType.Description := 'Standard shipment';
        DocType.Insert(true);

        DocType.Get('SS');
        
        // Manual assertions since we don't have Assert in System App
        if DocType.Description <> 'Standard shipment' then
            Error('Description mismatch.');
    end;
}

That [TransactionModel::AutoRollback] bit is important – it means any data we create during the test gets cleaned up automatically. No test pollution, no manual cleanup needed.

Testing the Tricky Stuff

Okay, let’s get into some more interesting cases. What happens when things should fail?

[Test]
[TransactionModel(TransactionModel::AutoRollback)]
procedure DuplicatePrimaryKey_Fails()
var
    DocType: Record "Type of Document Shipping";
    DocTypeCopy: Record "Type of Document Shipping";
    UniqueCode: Code[20];
    Inserted: Boolean;
begin
    // Create one record
    UniqueCode := 'TESTING';
    DocType.Init();
    DocType."Type of Legal Doc. Shipping" := UniqueCode;
    DocType.Description := 'Test document';
    DocType.Insert(true);
    
    // Try to create another with the same key
    DocTypeCopy.Init();
    DocTypeCopy."Type of Legal Doc. Shipping" := UniqueCode;
    DocTypeCopy.Description = 'Test document';
    
    Inserted := DocTypeCopy.Insert(false);
    
    // This should fail - duplicate keys aren't allowed
    if Inserted then
        Error('Duplicate record was created!');
end;

Notice we’re using Insert(false) here instead of Insert(true). That false means “don’t run field validation” – useful when you want to test the raw database constraint without triggering business logic.

Testing Validations and Business Rules

Most tables have validation rules. Let’s make sure they’re working:

[Test]
[TransactionModel(TransactionModel::AutoRollback)]
procedure ValidateNoCopy_Fails()
var
    DocType: Record "Type of Document Shipping";
begin
    DocType.Init();
    
    // Our table has a validation that No. Copy can't be > 5
    // Let's test that it properly rejects bad values
    asserterror
        DocType.Validate("No. Copy", 8);
        
    // If we get here without an error being thrown, the test fails
    // The asserterror keyword catches the error and continues
end;

The asserterror construct is super useful for negative testing. It says “I expect this next line to throw an error, and if it doesn’t, that’s a test failure.”

Testing Page Interactions

Sometimes you need to test that UI elements work correctly. Here’s how you test a page field:

[Test]
[TransactionModel(TransactionModel::AutoRollback)]
procedure FieldOnTransferOrderPage_Works()
var
    TransferHeader: Record "Transfer Header";
    TransferPage: TestPage "Transfer Order";
begin
    // First, make sure our test data exists
    EnsureDocTypeExists('SS');
    
    // Create a transfer header to work with
    TransferHeader.Init();
    TransferHeader."No." := 'TR-SS-001';
    TransferHeader.Insert(true);
    
    // Open the page, find our record, set a value
    TransferPage.OpenEdit();
    TransferPage.GoToRecord(TransferHeader);
    TransferPage."Type of Legal Doc. Shipping".SetValue('SS');
    TransferPage.Close();
    
    // Verify the change was saved
    TransferHeader.Get('TR-SS-001');
    
    if TransferHeader."Type of Legal Doc. Shipping" <> 'SS' then
        Error('Field on Transfer Order was not saved!');
end;

TestPages let you simulate user interactions. They’re a bit slower than pure code tests but invaluable for catching UI-related issues.

Helper Methods: Keeping Tests Clean

You’ll notice I used EnsureDocTypeExists() above. Here’s what that looks like:

local procedure EnsureDocTypeExists(Code: Code[10])
var
    DocType: Record "Type of Document Shipping";
begin
    // Only create if it doesn't exist
    if DocType.Get(Code) then
        exit;
        
    DocType.Init();
    DocType."Type of Legal Doc. Shipping" := Code;
    DocType.Description := 'Autocreated by test';
    DocType.Insert(true);
end;

Helpers like this keep your test methods focused on the actual test logic, not setup boilerplate.

Working Example

codeunit 51050 "Basic Tests"
{
    Subtype = Test;
    TestPermissions = Disabled;

    // =====================================================
    // 1. BASIC INSERT – RECORD IS CREATED CORRECTLY
    // =====================================================
    [Test]
    [TransactionModel(TransactionModel::AutoRollback)]
    procedure InsertNewDocumentType_Succeeds()
    var
        DocType: Record "Type of Document Shipping";
    begin

        DocType.Init();
        DocType."Type of Legal Doc. Shipping" := 'SS';
        DocType.Description := 'Standard shipment';
        DocType."Electronic Invoice" := true;
        DocType."No. Copy" := 3;
        DocType.Insert(true);

        DocType.Get('SS');

        if DocType.Description <> 'Standard shipment' then
            Error('Description mismatch.');

        if DocType."No. Copy" <> 3 then
            Error('Number of copies should be 3.');
    end;

    // =====================================================
    // 2. PRIMARY KEY UNIQUENESS 
    // =====================================================
    [Test]
    [TransactionModel(TransactionModel::AutoRollback)]
    procedure DuplicatePrimaryKey_Fails()
    var
        DocType: Record "Type of Document Shipping";
        DocTypeCopyCantBeCreated: Record "Type of Document Shipping";
        UniqueCode: Code[20];
        Inserted: Boolean;
    begin
        UniqueCode := 'TESTING';
        DocType.Init();
        DocType."Type of Legal Doc. Shipping" := UniqueCode;
        DocType.Description := 'Test document';
        DocType.Insert(true);

        DocTypeCopyCantBeCreated.Init();
        DocTypeCopyCantBeCreated."Type of Legal Doc. Shipping" := UniqueCode;
        DocTypeCopyCantBeCreated.Description := 'Test document';

        //WRONG VERSION - can't be done because of throwing exception
        // - Microsoft.Dynamics.Nav.Types.Exceptions.NavNCLAssertErrorException
        //asserterror
        //  DocTypeCopyCantBeCreated.Insert(false);

        //Correct version
        Inserted := DocTypeCopyCantBeCreated.Insert(false);

        if Inserted then
            Error('Duplicate record was created!');
    end;


    [Test]
    [TransactionModel(TransactionModel::AutoRollback)]
    procedure ValidateNoCopy_Fails()
    var
        DocType: Record "Type of Document Shipping";
    begin
        // GIVEN 
        DocType.Init();

        // WHEN + THEN – on our validation trigger it can't be greater then 5
        asserterror
            DocType.Validate("No. Copy", 8);
    end;

    // =====================================================
    // 3. TABLE RELATION VALIDATION
    // =====================================================
    [Test]
    [TransactionModel(TransactionModel::AutoRollback)]
    procedure InvalidNoSeries_Fails()
    var
        DocType: Record "Type of Document Shipping";
    begin
        DocType.Init();
        DocType."Type of Legal Doc. Shipping" := 'BAD';

        asserterror
            DocType.Validate("Posted Shipment Nos.", 'NONEXISTENT');
    end;

    // =====================================================
    // 4. PAGE TEST – FIELD IS EDITABLE AND SAVED
    // =====================================================
    [Test]
    [TransactionModel(TransactionModel::AutoRollback)]
    procedure FieldOnTransferOrderPage_Works()
    var
        TransferHeader: Record "Transfer Header";
        TransferPage: TestPage "Transfer Order";
    begin
        EnsureInventorySetup();
        EnsureDocTypeExists('SS');

        TransferHeader.Init();
        TransferHeader."No." := 'TR-SS-001';
        TransferHeader.Insert(true);

        TransferPage.OpenEdit();
        TransferPage.GoToRecord(TransferHeader);
        TransferPage."Type of Legal Doc. Shipping".SetValue('SS');
        TransferPage.Close();

        TransferHeader.Get('TR-SS-001');

        if TransferHeader."Type of Legal Doc. Shipping" <> 'SS' then
            Error('Field on Transfer Order was not saved!');
    end;

    // =====================================================
    // HELPERS
    // =====================================================
    local procedure EnsureDocTypeExists(Code: Code[10])
    var
        DocType: Record "Type of Document Shipping";
    begin
        if DocType.Get(Code) then
            exit;

        DocType.Init();
        DocType."Type of Legal Doc. Shipping" := Code;
        DocType.Description := 'Autocreated by test';
        DocType.Insert(true);
    end;

    local procedure EnsureInventorySetup()
    var
        InventorySetup: Record "Inventory Setup";
    begin
        if InventorySetup.Get() then
            exit;

        InventorySetup.Init();
        InventorySetup.Insert(true);
    end;
}

Running Your Tests

Once you’ve written your tests, you need to run them. From PowerShell, it’s straightforward:

PowerShell
Invoke-NAVCodeunit -ServerInstance CHILE-DEMO26 -CodeunitId 51050 -Company "CRONUS International Ltd."

You’ll get output like this:

WARNING: Test Codeunit 51050 Basic Tests
  InsertNewDocumentType_Succeeds: SUCCESS
  DuplicatePrimaryKey_Fails: SUCCESS
  ValidateNoCopy_Fails: SUCCESS
  InvalidNoSeries_Fails: SUCCESS
  FieldOnTransferOrderPage_Works: SUCCESS
SUCCESS

“Green” checkmarks (or “SUCCESS” messages) are what you want to see. If something fails, you’ll get an error message telling you exactly what went wrong and in which test.

Some Practical Advice

Start small. Don’t try to test everything at once. Pick one table or one process and write a few tests for it. See how it feels.

Name your tests clearly. ValidateNoCopy_Fails tells me exactly what’s being tested. Test1 doesn’t.

Use AutoRollback religiously. It makes tests independent and prevents weird issues where test A affects test B.

Test both the happy path and the error cases. It’s just as important to know that invalid data gets rejected as it is to know that valid data gets accepted.

Don’t test Microsoft’s code. Focus on your custom logic. If you’re testing whether Record.Insert() works… well, that’s Microsoft’s problem, not yours.

Common Pitfalls to Avoid

I’ve seen people get tripped up by permissions. That TestPermissions = Disabled at the top of the codeunit? That’s there so your tests run without worrying about user permissions. In real life, permissions matter, but in tests, they just get in the way.

Another one: forgetting that tests run in a transaction. If you’re trying to test something that commits data outside the transaction (like sending an email or calling a web service), you’ll need to mock those dependencies.

Also, watch out for hardcoded values that might exist in one company but not another. That’s why in our example we use EnsureDocTypeExists() – it creates the data if it’s not there.

Where to Go From Here

Once you’re comfortable with basic tests, you might want to look into:

  • Test libraries (pre-built helpers for creating test data)
  • Handler functions for testing messages and confirmations
  • Performance testing for slow operations
  • Integration testing across multiple components

But honestly? Start with what we’ve covered here. Get a few tests running. See how they help you catch bugs. The fancy stuff can wait.

The truth is, any testing is better than no testing. Even a handful of simple tests can save you hours of debugging down the line. So pick one piece of your code that’s causing problems and write a test for it. You might be surprised how quickly it becomes a habit.

Remember, the goal isn’t 100% test coverage or perfect tests. The goal is more reliable code and fewer midnight support calls. And from where I’m sitting, that’s definitely worth a bit of extra typing.

Leave a Reply

Your email address will not be published. Required fields are marked *