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:
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.
