A unit is any code that encapsulates an implementation. It could be one or a combination of individual functions or triggers.Thus, a unit could also refer to a local function. A unit, from the C/AL perspective, has the following:
A unit test must pass an input to the unit and verify that the output is as expected. It should be coded as a procedure in a codeunit of Test subtype.
Since unit tests are written to safeguard the implementation contract, they are not necessarily tests for functional requirements. The latter is the domain of functional tests. A successful refactoring project may result in changing some unit tests, but must not make any functional tests fail.
Since unit tests are quick low-level tests, it is affordable to have many of them. A larger number of unit tests make it possible to have a smaller number of functional tests, as the base behavior already gets tested as one goes towards higher-level tests.
Typically a unit test consists of the following sections.
In some cases, unit tests may also be used as an alternative to full functional tests, because:
With the perspective of the above objectives, a list of best practices has been drafted, which may serve as a guideline to those who create unit tests. The list should be treated as an addition to already existing best practices for writing good C/AL test code.
LOCAL PROCEDURE CreateItemWithBaseUOM@1(VAR Item@1000 : Record 27);
VAR
ItemUnitOfMeasure@1001 : Record 5404;
BEGIN
Item.INIT;
Item.INSERT;
ItemUnitOfMeasure.Code := 'NewCode';
ItemUnitOfMeasure.INSERT;
Item."Base Unit of Measure" := ItemUnitOfMeasure.Code;
Item.MODIFY;
END;
The EXERCISE step:
CreateInvtPickMovement.SetAssemblyLine(AssemblyLine);
CreateInvtPickMovement.SetInvtMovement(TRUE);
BinContent.SETRANGE("Item No.",BinContent."Item No.");
WhseGetBinContent.SETTABLEVIEW(BinContent);
WhseGetBinContent.USEREQUESTPAGE(FALSE);
WhseGetBinContent.InitializeReport(WhseWorksheetLine,WhseInternalPutAwayHeader,0);
WhseGetBinContent.RUN;
AssemblyOrderTestPage.OPENEDIT;
AssemblyOrderTestPage.ShowAvailability.INVOKE;
// The next line tests the InsertPickOrMoveBinWhseActLine procedure
Examples are based on the W1 application code in Microsoft Dynamics NAV 2013.
Business purpose
Test that changing the Costing Method on an item to Specific leads to an error if the tracking code is not Serial number specific.
The unit
OnValidate trigger of the Costing Method field on the Item table:
...IF "Costing Method" = "Costing Method"::Specific THEN BEGIN
IF NOT ItemTrackingCode."SN Specific Tracking" THEN
ERROR( Text018,
...
The test
[Test] PROCEDURE VerifyErrorRaisedOnChangingCostingMethodToSpecific@1(); VAR ItemTrackingCode@1001 : Record 6502; Item@1000 : Record 27; BEGIN // Changing the Costing Method on an Item card to Specific // leads to an error if the tracking code is not Serial number specific
// SETUP : Make item tracking code without SN Specific Tracking ItemTrackingCode.INIT; ItemTrackingCode.Code := 'MyITCode'; ItemTrackingCode."SN Specific Tracking" := FALSE; ItemTrackingCode.INSERT;
// SETUP : Make item with above item tracking code Item.INIT; Item."No." := 'MyItem'; Item."Item Tracking Code" := ItemTrackingCode.Code;
// EXERCISE : Validate Costing method to Specific ASSERTERROR Item.VALIDATE("Costing Method",Item."Costing Method"::Specific);
// VERIFY : error message IF STRPOS(GETLASTERRORTEXT,'SN Specific Tracking must be Yes') <= 0 THEN ERROR('Wrong error message'); END;
Observations
The target unit was a very specific line and the above test was therefore short and precise.
Example 2
Test that posting a sales order creates a posted shipment line with the correct quantity.
The OnRun trigger in Sales-Post codeunit.
[Test] PROCEDURE TestPostedSalesQuantityAfterPosting@2(); VAR Item@1001 : Record 27; SalesHeader@1000 : Record 36; SalesLine@1002 : Record 37; SalesShipmentLine@1003 : Record 111; ItemUnitOfMeasure@1005 : Record 5404; Quantity@1004 : Decimal; BEGIN // Post a sales order and verify the posted shipment line quantity.
// SETUP : Make item with above item tracking code Item.INIT; Item."No." := 'MyItem'; // Create unit of measure ItemUnitOfMeasure."Item No." := Item."No."; ItemUnitOfMeasure.Code := 'PCS'; ItemUnitOfMeasure.INSERT; Item."Base Unit of Measure" := ItemUnitOfMeasure.Code; Item."Inventory Posting Group" := 'RESALE'; Item.INSERT;
// SETUP : Create sales header with item and quantity SalesHeader."Document Type" := SalesHeader."Document Type"::Order; SalesHeader."No." := 'MySalesHeaderNo'; SalesHeader."Sell-to Customer No." := '10000'; SalesHeader."Bill-to Customer No." := SalesHeader."Sell-to Customer No."; SalesHeader."Posting Date" := WORKDATE; SalesHeader."Document Date" := SalesHeader."Posting Date"; SalesHeader."Due Date" := SalesHeader."Posting Date"; SalesHeader.Ship := TRUE; SalesHeader.Invoice := TRUE; SalesHeader."Shipping No. Series" := 'S-SHPT'; SalesHeader."Posting No. Series" := 'S-INV+'; SalesHeader."Dimension Set ID" := 4; SalesHeader.INSERT;
// SETUP : Create the sales line SalesLine."Document Type" := SalesHeader."Document Type"; SalesLine."Document No." := SalesHeader."No."; SalesLine.Type := SalesLine.Type::Item; SalesLine."No." := Item."No."; Quantity := 7; SalesLine.Quantity := Quantity; SalesLine."Quantity (Base)":= Quantity; SalesLine."Qty. to Invoice" := Quantity; SalesLine."Qty. to Invoice (Base)" := Quantity; SalesLine."Qty. to Ship" := Quantity; SalesLine."Qty. to Ship (Base)" := Quantity; SalesLine."Gen. Prod. Posting Group" := 'RETAIL'; SalesLine."Gen. Bus. Posting Group" := 'EU'; SalesLine."VAT Bus. Posting Group" := 'EU'; SalesLine."VAT Prod. Posting Group" := 'VAT25'; SalesLine."VAT Calculation Type" := SalesLine."VAT Calculation Type"::"Reverse Charge VAT"; SalesLine.INSERT;
// EXERCISE : Call the codeunit to post sales header CODEUNIT.RUN(CODEUNIT::"Sales-Post",SalesHeader);
// VERIFY : A Posted Shipment Line is created with the same quantity SalesShipmentLine.SETRANGE("Order No.",SalesHeader."No."); SalesShipmentLine.FINDLAST; SalesShipmentLine.TESTFIELD(Quantity,Quantity); END;
The targeted unit is large and there are many lines before the code to set the quantity on the Sales Shipment Line is reached. In order to reach this line a large setup is needed in the test as well, which makes the unit test bulky. It may be better to write a functional test in this case.
- Soumya Dutta from the NAV team
Wow, I didn't new about TestIsolation property. Until now I've used
ASSERTERROR ERROR('');
at the end of each test to rollback changes.
TestIsolation is better because it rollbacks also if there was COMMIT called :-)
Yes Seer,
The TestIsolation property is quite useful. Using ASSERTERROR ERROR(''); may have unintended consequences if you do not call CLEARLASTERRORTEXT subsequently.
But it is handled better using the TestIsolation property.
Cheers,
Dutta.