The pipeline post said to write tests. This is how to write ones that run reliably: the Given-When-Then shape, building data with the Library codeunits, handling dialogs and pages with handler functions, asserting errors, test isolation so one test does not pollute the next, and the two mistakes that make a test suite flaky.
A test that fails randomly is worse than no test, because the team learns to ignore red. These patterns produce tests that pass when the code is right and fail when it is not, every time.
[Test]
[HandlerFunctions('ConfirmYesHandler')]
procedure ReleasingShipmentWithoutCarrierFails()
var
Shipment: Record "CON Courier Shipment";
ShipmentMgt: Codeunit "CON Courier Shipment Mgt.";
begin
// [GIVEN] a shipment with no carrier
CreateShipment(Shipment);
Shipment.Carrier := Shipment.Carrier::" ";
Shipment.Modify();
// [WHEN] releasing it
asserterror ShipmentMgt.Release(Shipment);
// [THEN] the carrier error is raised
Assert.ExpectedError(CarrierMissingErr);
end;
The comments are a convention Microsoft uses and tooling recognises. One rule per test; the name says what is being proven.
Never depend on the demo company. The test library apps (Tests-TestLibraries from Microsoft's test toolkit) give you LibrarySales.CreateCustomer, LibraryInventory.CreateItem, LibrarySales.CreateSalesDocumentWithItem, LibraryERM.CreateGLAccount, LibraryUtility.GenerateRandomCode and hundreds more, each creating a valid record with random keys so tests do not collide. Wrap your own entity the same way in a "CON Library - Courier" test codeunit: CreateShipment(var Shipment) that sets everything a valid shipment needs.
Any dialog the code under test would show has to be answered by the test, or the test hangs. [ConfirmHandler], [MessageHandler], [PageHandler], [ModalPageHandler], [RequestPageHandler], [StrMenuHandler] procedures are declared on the test codeunit and listed in [HandlerFunctions] on each test that needs them. A test that declares a handler that is not called fails, which is deliberate: it tells you the code path changed.
[ConfirmHandler]
procedure ConfirmYesHandler(Question: Text[1024]; var Reply: Boolean)
begin
Reply := true;
end;
TestIsolation = Codeunit (on the test runner) rolls back the database after each test codeunit; Function after each test. Set the runner to function-level isolation and every test starts from the same state. Within a test, do not rely on anything a previous test created. Single-instance codeunits keep state across tests in the same session: clear them in an Initialize procedure called at the start of each test, which is also where you set up setup records once per run (guarded by an IsInitialized flag).
Call the base operation that raises the event (post a document with the library) and assert the effect. For subscribers you want active only in a test, use EventSubscriberInstance = Manual and BindSubscription(TestSubscriberCodeunit) inside the test, with a handler that captures what was passed.
Run the suite under a restricted permission set (LibraryLowerPermissions.SetO365BusFull and friends) at least once per run, so a missing permission in your shipped sets fails a test rather than a user.
WorkDate versus Today versus a hard-coded date; posting-date ranges; tests that pass until month-end. Use WorkDate() consistently and set it in Initialize.Twenty tests like this, in the pipeline on every commit, are what let you deploy on a Tuesday afternoon without a knot in your stomach.