Test the advanced sample extension
It's required to submit tests with your extension in order to pass validation. This walkthrough builds on the advanced sample extension, which you can read about here Building an Advanced Sample Extension. If you're new to building extensions, we suggest that you get familiar with Building your first sample extension that uses new objects and extension objects. This walkthrough goes through how you develop the test for the sample CustomerRewards extension.
For information about submitting your app to AppSource, see Checklist for Submitting Your App.
Prerequisites
To complete this walkthrough, you need:
- Dynamics 365 Business Central Docker container-based development environment For more information, see Get started with the Container Sandbox Development Environment and Running a Container-Based Development Environment
- Visual Studio Code
- The AL Language extension for Microsoft Dynamics 365 Business Central for Visual Studio Code
Identify the areas of the extension that need to be tested
Before writing tests for your extension, you need to identify all the areas of the extension that need to be tested.
- Ensure that your tests cover all the setup and usage scenario steps found in the user scenario document. This includes Assisted Setup, pages, fields, actions, events, and other controls and objects used by your extension.
- The CRONUS demo company is used in this walkthrough. If your app requires setup within the core product or any more data, remember to include that in your tests.
- As part of your tests, remember to include tests that verify that the extension works as expected for a user that does not have SUPER permissions. For more information, see Special Permission Sets.
- Your tests should not make any requests to an external service. Mock your external calls to prevent this from happening.
In the sample test, we consider the following:
Logic in our Install codeunit.
Assisted Setup - Customer Rewards Wizard page. We verify that the wizard behaves as expected. It can be used to completion without errors. The Assisted Setup contains code that mimics making calls to an external service or API. Because our tests can't make requests to an external service, we mock the requests and the responses.
Reward Level page. We verify that the page behaves as expected when the user opens it whether Customer Rewards is activated or not.
Customer List page. We verify that our new Reward Levels action exists on the page and that it behaves as expected whether the extension is activated or not.
Customer Card page. We verify that the page has the Reward Level and Reward Points field that we added.
New Customer should have zero reward points and corresponding reward level if defined.
Different scenarios involving Customers and Sales Orders to verify that Reward Points work as expected and that reward levels for reward points work as defined by the user.
Each test also verifies that the extension works for a user that doesn't have SUPER permissions.
Write the tests
We first create a new project (CustomerRewardsTest) for the tests. You're required to separate the CustomerRewards extension and the tests into separate projects.
Before we can start writing the tests for the extension, we need to do the following:
- Specify the dependencies between the extension (CustomerRewards) and the test (CustomerRewardsTest) projects.
Our CustomerRewardsTest project is referencing objects from the CustomerRewards project and so we need to specify this in thedependencies
setting in the CustomerRewardsTest project's app.json file. Thedependencies
setting takes a list of dependencies, where each dependency specifies theappId
,name
,publisher
, andversion
of the base project/package that the current project/package depends on.
Note
Another prerequisite is to update the app.json with the dependencies of the test libraries used. In this case Library Assert and Tests-TestLibraries.
{
...
"dependencies": [
{
"appId": "c228bdcf-7112-480b-a832-da81971b6feb",
"name": "CustomerRewards",
"publisher": "Microsoft",
"version": "1.0.0.0"
},
{
"id": "dd0be2ea-f733-4d65-bb34-a28f4624fb14",
"name": "Library Assert",
"publisher": "Microsoft",
"version": "19.0.0.0"
},
{
"id": "5d86850b-0d76-4eca-bd7b-951ad998e997",
"name": "Tests-TestLibraries",
"publisher": "Microsoft",
"version": "19.0.0.0"
}
]
...
}
For more information, see JSON Files.
After setting the dependencies
value, you are prompted to download the symbols from the base project/package if they aren't present.
Application Test Toolkit
We are using the Application Test Toolkit to automate and run the tests that we write. The toolkit includes:
Codeunits with test functions to test various application areas.
Codeunits with generic and application-specific functions to reduce duplication of test code.
Application objects for running application tests such as the Test Tool page.
In order to install the Application Test Toolkit:
- Open the BCContainerHelper prompt found on the Desktop. You see a list of functions that you can run on the container.
- Run the
Import-TestToolkitToBCContainer
function with-containerName
parameter to import the test toolkit into the application database.
Import-TestToolkitToBCContainer -containerName <name-of-container>
Alternatively, if you use the New-BCContainer
function from the BCContainerHelper PowerShell module to create your containers on Docker, you can add the -includeTestToolkit
flag. This installs the Application Test Toolkit during the creation of your container.
Without further configuration, the Import-TestToolkitToBCContainer and New-BCContainer with -includeTestToolkit will install the framework, the libraries, and all base application tests. Both the Import-TestToolkitToBCContainer and New-BCContainer cmdlets support two more parameters, which limit the number of apps installed:
-includeTestFrameworkOnly
installs the Test Framework only. This option includes the Test Runner and low-level functions such as Any and Assert.-includeTestLibrariesOnly
installs the Test Framework and the Test Libraries only. Beside the Test Framework, this option includes functionality that is shared between base application tests.
Describe your tests
To help you design the relevant tests for your functionality, you can write scenarios that outline what you want to test, and you can write test criteria in the GIVEN-WHEN-THEN format. By adding comments based on feature, scenario, and GIVEN-WHEN-THEN, you add structure to your test code and make tests readable.
The following sections provide an overview of the tags that we recommend you to use.
FEATURE Tag
// [FEATURE] [<FeatureTag1>] [<FeatureTagN>]
FeatureTag
represents the name of the feature, application area, functional area, or another aspect of the application. This list of tags must point to an area of your solution that is touched by the test. Order tags in descending importance. Start with the most important tags referring to the WHEN or THEN steps. The [FEATURE]
tag can be set for the whole test codeunit. This means all tests in this codeunit inherit the list of tags set there. If a test is supposed to have the same list of tags as the codeunit has, you don't have to add the [FEATURE]
tag for this test. Add the tags only if the test has something specific to say.
SCENARIO Tag
// [SCENARIO <ScenarioID>] <TestDescription>`
ScenarioID
links the test to a work item for the functionality. For example, if you use Visual Studio Codespace or Azure DevOps Server, [SCENARIO 12345]
represents a work item with the ID 12345.
TestDescription represents a short description of the purpose of the test, such as Annie can apply a deferral template to a purchase order.
GIVEN-WHEN-THEN Tags
The GIVEN-WHEN-THEN
tags provide a framework for the specific test criteria.
Tag | Description |
---|---|
GIVEN | Describes one step in setting up the test. If you feel a need to add an AND, you should probably add a separate GIVEN. In most of cases, in order to run an action under test, you must prepare the database. Tests can be complex, so you can add more than one GIVEN. They can come in one block or comment particular lines of code. Don't try to repeat code and comment each line. Instead, add information of a higher level that would be valuable when reading without the test code. |
WHEN | Describes the action under test. A test is to test one thing. There should be only one WHEN in a test. It's the line of code that changes the state of something that we're going to verify. If you feel a need to add more than one WHEN followed by different verification, you should split this test in two or more tests. |
THEN | Describes what is verified by the test. All tests must have a verification part. If there's no verification, the test doesn't test anything. You can add more than one THEN tag. |
We can now begin writing the tests for the extension.
MockCustomerRewardsExtMgt codeunit object
The 50102 MockCustomerRewardsExtMgt codeunit contains all the code that mocks the process of validating the activation code for Customer Rewards. Because we can't make requests to external services in the tests, we define a subscriber method MockOnGetActivationCodeStatusFromServerSubscriber for handling the OnGetActivationCodeStatusFromServer event when it's raised in the Customer Rewards Ext. Mgt. codeunit. The EventSubscriberInstance property for this codeunit is set to Manual so that we can control when the subscriber function is called. We want the subscriber method to be called only during our tests. We also define a Setup procedure that modifies the Customer Rewards Ext. Mgt. Codeunit ID in the Customer Rewards Mgt. Setup table so that the actual OnGetActivationCodeStatusFromServerSubscriber won't handle OnGetActivationCodeStatusFromServer event when it's raised.
codeunit 50102 MockCustomerRewardsExtMgt
{
// When set to Manual subscribers in this codeunit are bound to an event by calling the BINDSUBSCRIPTION method.
// This enables you to control which event subscriber instances are called when an event is raised.
// If the BINDSUBSCRIPTION method is not called, then nothing will happen when the published event is raised.
EventSubscriberInstance = Manual;
var
DummyResponseTxt: Text;
DummySuccessResponseTxt: Label '{"ActivationResponse": "Success"}', Locked = true;
DummyFailureResponseTxt: Label '{"ActivationResponse": "Failure"}', Locked = true;
// Mocks the response text for testing success and failure scenarios
procedure MockActivationResponse(Success: Boolean)
begin
if Success then
DummyResponseTxt := DummySuccessResponseTxt
else
DummyResponseTxt := DummyFailureResponseTxt;
end;
// Modifies the default Customer Rewards Ext. Mgt codeunit to this codeunit to prevent the
// OnGetActivationCodeStatusFromServerSubscriber in Customer Rewards Ext. Mgt from handling
// the OnGetActivationCodeStatusFromServer event when it is raised
procedure Setup()
var
CustomerRewardsExtMgtSetup: Record "Customer Rewards Mgt. Setup";
begin
CustomerRewardsExtMgtSetup.Get;
CustomerRewardsExtMgtSetup."Customer Rewards Ext. Mgt. Codeunit ID" := Codeunit::MockCustomerRewardsExtMgt;
CustomerRewardsExtMgtSetup.Modify;
end;
// Subscribes to OnGetActivationCodeStatusFromServer event and handles it when the event is raised
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Customer Rewards Ext. Mgt.", 'OnGetActivationCodeStatusFromServer', '', false, false)]
local procedure MockOnGetActivationCodeStatusFromServerSubscriber(ActivationCode: Text)
var
ActivationCodeInfo: Record "Activation Code Information";
ResponseText: Text;
Result: JsonToken;
JsonRepsonse: JsonToken;
begin
if(MockGetHttpResponse(ActivationCode, ResponseText)) then begin
JsonRepsonse.ReadFrom(ResponseText);
if(JsonRepsonse.SelectToken('ActivationResponse', Result)) then begin
if(Result.AsValue().AsText() = 'Success') then begin
if ActivationCodeInfo.FindFirst then
ActivationCodeInfo.Delete;
ActivationCodeInfo.Init;
ActivationCodeInfo.ActivationCode := ActivationCode;
ActivationCodeInfo."Date Activated" := Today;
ActivationCodeInfo."Expiration Date" := CALCDATE('<1Y>', Today);
ActivationCodeInfo.Insert;
end;
end;
end;
end;
// Mocks making calls to external service
local procedure MockGetHttpResponse(ActivationCode: Text; var ResponseText: Text): Boolean
begin
if ActivationCode = '' then
exit(false);
ResponseText := DummyResponseTxt;
exit(true);
end;
}
Customer Rewards Test codeunit object
A test codeunit must have its Subtype property set to Test and the test methods must be decorated with the [Test]
attribute. When a test codeunit runs, it executes the OnRun trigger, and then executes each test method in the codeunit. By default, each test function runs in a separate database transaction, but you can use the TransactionModel attribute on test methods to control the transactional behavior. The outcome of a test method is either SUCCESS or FAILURE. If any error is raised by either the code that is being tested or the test code, then the outcome is FAILURE and the error is included in the results log file. Even if the outcome of one test method is FAILURE, the next test methods are still executed.
In addition to the Application Test Toolkit, the following features are available to help you test your extension:
Test pages
Test pages mimic actual pages, but don't present any UI on a client computer. Test pages let you test the code on a page by using AL to simulate user interaction with the page. You can access the fields on a page and the properties of a page or a field by using the dot notation. You can open and close test pages, perform actions on the test page, and navigate around the test page by using AL methods.
UI handlers
To create tests that can be automated, you must handle cases when user interaction is requested by code that is being tested. UI handlers run instead of the requested UI. UI handlers provide the same exit state as the UI. For example, a test method that has a ConfirmHandler handles CONFIRM method calls. If code that is being tested calls the CONFIRM method, then the ConfirmHandler method is called instead of the CONFIRM method. You write code in the ConfirmHandler method to verify that the expected question is displayed by the CONFIRM method and you write AL code to return the relevant reply. The following table describes the available UI handlers.
Function Type | Syntax example | Purpose |
---|---|---|
MessageHandler | [MessageHandler] procedure MessageHandler(Msg : Text[1024]) |
This handler is called when a message function is invoked in the code. The parameter type, Text, contains the text of the function. |
ConfirmHandler | [ConfirmHandler] procedure ConfirmHandlerNo(Question: Text[1024]; var Reply: Boolean) |
This handler is called when a confirm function is invoked in the code. The parameter type, Text, contains the text of the function and the parameter Reply if the response to confirm is yes or no. |
StrMenuHandler | [StrMenuHandler] procedure StrMenuHandler(Option: Text[1024]; var Choice: Integer; Instruction: Text[1024]) |
This handler is called when a StrMenu function is invoked in code. The parameter type, Text, contains the text of the function and Choice is the option chosen in the StrMenu. Options is the list of the different option values and Instruction is the leading text. |
PageHandler | [PageHandler] procedure MappingPageHandler(var MappingPage: TestPage 1214) |
This handler is called when a nonmodal page is invoked in the code. TestPage is the specific page in this case. |
ModalPageHandler | [ModalPageHandler] procedure DevSelectedObjectPageHandler(var DevSelectedObjects: TestPage 89015) |
This handler is called when a modal page is invoked in the code. TestPage is the specific page in this case. |
ReportHandler | [ReportHandler] procedure VendorListReportHandler(var VendorList: Report 301) |
This handler is called when a report is invoked in the code. Report is the specific report in this case. |
RequestPageHandler | [RequestPageHandler] procedure SalesInvoiceReportRequestPageHandler(var SalesInvoice: TestRequestPage 206) |
This handler is called when a report is invoked in the code. TestRequestPage refers to the specific report ID. |
You must create a specific handler for each page that you want to handle. Any unhandled UI in the test methods of the test codeunit causes a failure of the test.
ASSERTERROR statement
When you test your extension, you should test that your code performs as expected under both successful and failing conditions. These are called positive and negative tests. To test how your extension performs under failing conditions, you can use the ASSERTERROR keyword. The ASSERTERROR keyword specifies that an error is expected at run time in the statement that follows the ASSERTERROR keyword. If a simple or compound statement that follows the ASSERTERROR keyword causes an error, then execution successfully continues to the next statement in the test function. If a statement that follows the ASSERTERROR keyword doesn't cause an error, then the ASSERTERROR statement itself fails with an error, and the test function that is running produces a FAILURE result.
The 50103 Customer Rewards Test codeunit contains all the tests for the Customer Rewards extension. For each test method, we follow the following pattern:
Initialize and set up the conditions for the test.
Invoke the business logic that you want to test.
Validate that the business logic performed as expected.
Let us look some of the sample tests.
TestOnInstallLogic Test
This test verifies that the logic we defined in our Install codeunit works as expected. We first call a helper method Initialize which initializes and cleans up any objects that will be needed for the test. The Initialize method also binds our mock codeunit MockCustomerRewardsExtMgt to our test codeunit so that any events raised during our test can be handled by the subscriber methods specified in our mock codeunit.
Next, we invoke the SetDefaultCustomerRewardsExtMgtCodeunit method, which is the method defined in our Install codeunit.
And finally, we verify using the Assert codeunit from the Application Test Toolkit, that the Customer Rewards Mgt. Setup table contains the expected codeunit ID.
TestCustomerRewardsWizardActivationPageErrorsWhenInvalidActivationCodeEntered Test
This is one of the tests that focus on the Customer Rewards Assisted Setup Guide. The test verifies that an error message is displayed when a not valid activation code is entered in the wizard.
First, Initialize is called to clean up previous state and bind our mock subscriber methods to the test codeunit. Additionally, we set our MockActivationResponse to return FAILURE since we are mocking a not valid validation of the activation code. We also use the Library - Lower Permissions codeunit to restrict the users permission to one that doesn't have the SUPER permission.
Next, we open the Customer Rewards Wizard by using a Customer Rewards Wizard, the TestPage object is used to mimic the actual page. On the page, the activation code is entered and then the Activate action is invoked.
And finally, we verify that an error message is displayed because the validation of the activation code failed. If no other error is reported, then we're also able to conclude that the functionality in this test can be run without the need for a SUPER permission.
TestRewardLevelsActionExistsOnCustomerListPage Test
This test verifies that the new Reward Levels action exists on the Customer List page.
TestCustomerHasBronzeRewardLevelAfterPostedSalesOrders Test
This is one of the tests that considers the interaction between Customers, Sales Orders, and Reward Levels. This test verifies that when two sales orders are made for a new customer, that customer accrues two reward points. Consequently, they attain the corresponding reward level for two points, which is the BRONZE reward level.
First, the test is initialized by calling Initialize. The extension is activated and then a BRONZE reward level for two points or more is set up in the Reward Level table.
Next, a new Customer is created using the LibrarySales codeunit from the Application Test Toolkit. And then, the LibrarySales codeunit is used again to create and post two sales orders for the previously created customer.
Finally, to verify that the customer got the correct reward points and level, we open the Customer Card using its corresponding TestPage and then verify the values in the Reward Points and Reward Level fields.
There are many more areas that we look at in the sample test. See the full codeunit below for the rest of the tests.
codeunit 50103 "Customer Rewards Test"
{
// [FEATURE] [Customer Rewards]
Subtype = Test;
TestPermissions = Disabled;
var
Assert: Codeunit Assert;
LibraryLowerPermissions: Codeunit "Library - Lower Permissions";
LibrarySales: Codeunit "Library - Sales";
MockCustomerRewardsExtMgt: Codeunit MockCustomerRewardsExtMgt;
ActivatedTxt: Label 'Customer Rewards should be activated';
NotActivatedTxt: Label 'Customer Rewards should not be activated';
BronzeLevelTxt: Label 'BRONZE';
SilverLevelTxt: Label 'SILVER';
GoldLevelTxt: Label 'GOLD';
NoLevelTxt: Label 'NONE';
[Test]
procedure TestOnInstallLogic()
var
CustomerRewardsExtMgtSetup: Record "Customer Rewards Mgt. Setup";
CustomerRewardsInstallLogic: Codeunit "Customer Rewards Install Logic";
begin
// [Scenario] Check default codeunit is specified for handling events on install
// [Given] Customer Rewards Mgt. Setup table
Initialize;
// [When] Install logic is run
CustomerRewardsInstallLogic.SetDefaultCustomerRewardsExtMgtCodeunit;
// [Then] Default Customer Rewards Ext. Mgt codeunit is specified
Assert.AreEqual(1, CustomerRewardsExtMgtSetup.Count, 'CustomerRewardsExtMgtSetup must have exactly one record.');
CustomerRewardsExtMgtSetup.Get;
Assert.AreEqual(Codeunit::"Customer Rewards Ext. Mgt.", CustomerRewardsExtMgtSetup."Customer Rewards Ext. Mgt. Codeunit ID", 'Codeunit does not match default');
end;
[Test]
procedure TestCustomerRewardsWizardTermsPage()
var
CustomerRewardsWizardTestPage: TestPage "Customer Rewards Wizard";
begin
// [Scenario] Check Terms Page on Wizard
// [Given] The Customer Rewards Wizard
Initialize;
// Using permissions that do not include SUPER
LibraryLowerPermissions.SetO365BusFull;
// [When] The Wizard is opnened
CustomerRewardsWizardTestPage.OpenView;
// [Then] The terms page and fields behave as expected
Assert.IsFalse(CustomerRewardsWizardTestPage.EnableFeature.AsBoolean, 'Enable feature should be unchecked');
Assert.IsFalse(CustomerRewardsWizardTestPage.ActionNext.Visible, 'Next should not be visible');
Assert.IsFalse(CustomerRewardsWizardTestPage.ActionBack.Visible, 'Back should not be visible');
Assert.IsFalse(CustomerRewardsWizardTestPage.ActionFinish.Enabled, 'Finish should be disabled');
CustomerRewardsWizardTestPage.EnableFeature.SetValue(true);
Assert.IsTrue(CustomerRewardsWizardTestPage.EnableFeature.AsBoolean, 'Enable feature should be checked');
Assert.IsTrue(CustomerRewardsWizardTestPage.ActionNext.Visible, 'Next should be visible');
Assert.IsFalse(CustomerRewardsWizardTestPage.ActionFinish.Enabled, 'Finish should be disabled');
CustomerRewardsWizardTestPage.Close;
end;
[Test]
procedure TestCustomerRewardsWizardActivationPageErrorsWhenNoActivationCodeEntered()
var
CustomerRewardsExtMgt: Codeunit "Customer Rewards Ext. Mgt.";
CustomerRewardsWizardTestPage: TestPage "Customer Rewards Wizard";
begin
// [Scenario] Error message when user tries to activate Customer Rewards without activation code.
// [Given] The Customer Rewards Wizard
Initialize;
Commit;
// Using permissions that do not include SUPER
LibraryLowerPermissions.SetO365BusFull;
Assert.IsFalse(CustomerRewardsExtMgt.IsCustomerRewardsActivated, NotActivatedTxt);
// [When] User invokes activate action without entering activation code
OpenCustomerRewardsWizardActivationPage(CustomerRewardsWizardTestPage);
Assert.IsTrue(CustomerRewardsWizardTestPage.ActionBack.Visible, 'Back should be visible');
Assert.IsFalse(CustomerRewardsWizardTestPage.ActionFinish.Enabled, 'Finish should be disabled');
// [Then] Error message displayed
asserterror CustomerRewardsWizardTestPage.ActionActivate.Invoke;
Assert.AreEqual(GETLASTERRORTEXT, 'Activation code cannot be blank.', 'Invalid error message.');
Assert.IsFalse(CustomerRewardsExtMgt.IsCustomerRewardsActivated, NotActivatedTxt);
end;
[Test]
procedure TestCustomerRewardsWizardActivationPageErrorsWhenShorterActivationCodeEntered()
var
CustomerRewardsExtMgt: Codeunit "Customer Rewards Ext. Mgt.";
CustomerRewardsWizardTestPage: TestPage "Customer Rewards Wizard";
begin
// [Scenario] Error message when user tries to activate Customer Rewards with short activation code.
// [Given] The Customer Rewards Wizard
Initialize;
Commit;
// Using permissions that do not include SUPER
LibraryLowerPermissions.SetO365BusFull;
Assert.IsFalse(CustomerRewardsExtMgt.IsCustomerRewardsActivated, NotActivatedTxt);
// [When] User invokes activate action after entering short activation code
OpenCustomerRewardsWizardActivationPage(CustomerRewardsWizardTestPage);
CustomerRewardsWizardTestPage.Activationcode.SetValue('123456');
// [Then] Error message displayed
asserterror CustomerRewardsWizardTestPage.ActionActivate.Invoke;
Assert.AreEqual(GETLASTERRORTEXT, 'Activation code must have 14 digits.', 'Invalid error message.');
Assert.IsFalse(CustomerRewardsExtMgt.IsCustomerRewardsActivated, NotActivatedTxt);
end;
[Test]
procedure TestCustomerRewardsWizardActivationPageErrorsWhenLongerActivationCodeEntered()
var
CustomerRewardsExtMgt: Codeunit "Customer Rewards Ext. Mgt.";
CustomerRewardsWizardTestPage: TestPage "Customer Rewards Wizard";
begin
// [Scenario] Error message when user tries to activate Customer Rewards with long activation code.
// [Given] The Customer Rewards Wizard
Initialize;
Commit;
// Using permissions that do not include SUPER
LibraryLowerPermissions.SetO365BusFull;
Assert.IsFalse(CustomerRewardsExtMgt.IsCustomerRewardsActivated, NotActivatedTxt);
// [When] User invokes activate action after entering long activation code
OpenCustomerRewardsWizardActivationPage(CustomerRewardsWizardTestPage);
CustomerRewardsWizardTestPage.Activationcode.SetValue('123456789012345');
// [Then] Error message displayed
asserterror CustomerRewardsWizardTestPage.ActionActivate.Invoke;
Assert.AreEqual(GETLASTERRORTEXT, 'Activation code must have 14 digits.', 'Invalid error message.');
Assert.IsFalse(CustomerRewardsExtMgt.IsCustomerRewardsActivated, NotActivatedTxt);
end;
[Test]
procedure TestCustomerRewardsWizardActivationPageErrorsWhenInvalidActivationCodeEntered()
var
CustomerRewardsExtMgt: Codeunit "Customer Rewards Ext. Mgt.";
CustomerRewardsWizardTestPage: TestPage "Customer Rewards Wizard";
begin
// [Scenario] Error message when user tries to activate Customer Rewards with invalid activation code.
// [Given] The Customer Rewards Wizard
Initialize;
Commit;
// Using permissions that do not include SUPER
LibraryLowerPermissions.SetO365BusFull;
Assert.IsFalse(CustomerRewardsExtMgt.IsCustomerRewardsActivated, NotActivatedTxt);
MockCustomerRewardsExtMgt.MockActivationResponse(false);
// [When] User invokes activate action after entering invalid but correct length activation code
OpenCustomerRewardsWizardActivationPage(CustomerRewardsWizardTestPage);
CustomerRewardsWizardTestPage.Activationcode.SetValue('12345678901234');
// [Then] Error message displayed
asserterror CustomerRewardsWizardTestPage.ActionActivate.Invoke;
Assert.AreEqual(GETLASTERRORTEXT, 'Activation failed. Please check the activtion code you entered.', 'Invalid error message.');
Assert.IsFalse(CustomerRewardsExtMgt.IsCustomerRewardsActivated, NotActivatedTxt);
end;
[Test]
procedure TestCustomerRewardsWizardActivationPageDoesNotErrorWhenValidActivationCodeEntered()
var
CustomerRewardsExtMgt: Codeunit "Customer Rewards Ext. Mgt.";
CustomerRewardsWizardTestPage: TestPage "Customer Rewards Wizard";
begin
// [Scenario] Customer Rewards is activated when user enters valid activation code.
// [Given] The Customer Rewards Wizard
Initialize;
Commit;
// Using permissions that do not include SUPER
LibraryLowerPermissions.SetO365BusFull;
Assert.IsFalse(CustomerRewardsExtMgt.IsCustomerRewardsActivated, NotActivatedTxt);
MockCustomerRewardsExtMgt.MockActivationResponse(true);
// [When] User invokes activate action after entering valid activation code
OpenCustomerRewardsWizardActivationPage(CustomerRewardsWizardTestPage);
CustomerRewardsWizardTestPage.Activationcode.SetValue('12345678901234');
CustomerRewardsWizardTestPage.ActionActivate.Invoke;
CustomerRewardsWizardTestPage.Close;
// [Then] Customer Rewards is activated
Assert.IsTrue(CustomerRewardsExtMgt.IsCustomerRewardsActivated, ActivatedTxt);
end;
[Test]
procedure TestRewardsLevelListPageDoesNotOpenWhenNotActivated()
var
CustomerRewardsExtMgt: Codeunit "Customer Rewards Ext. Mgt.";
RewardLevelListTestPage: TestPage "Rewards Level List";
begin
// [Scenario] Error opening Reward Level Page when Customer Rewards is not activated
// [Given] Unactivated Customer Rewards
Initialize;
Commit;
// Using permissions that do not include SUPER
LibraryLowerPermissions.SetO365BusFull;
Assert.IsFalse(CustomerRewardsExtMgt.IsCustomerRewardsActivated, NotActivatedTxt);
// [When] User opens Reward Level Page
// [Then] Error message
asserterror RewardLevelListTestPage.OpenView;
Assert.AreEqual(GETLASTERRORTEXT, 'Customer Rewards is not activated', 'Invalid error message.');
end;
[Test]
procedure TestRewardsLevelListPageOpensWhenActivated()
var
CustomerRewardsExtMgt: Codeunit "Customer Rewards Ext. Mgt.";
RewardLevelListTestPage: TestPage "Rewards Level List";
begin
// [Scenario] Reward Level Page opens when Customer Rewards is activated
// [Given] Activated Customer Rewards
Initialize;
Commit;
// Using permissions that do not include SUPER
LibraryLowerPermissions.SetO365BusFull;
Assert.IsFalse(CustomerRewardsExtMgt.IsCustomerRewardsActivated, NotActivatedTxt);
ActivateCustomerRewards;
Assert.IsTrue(CustomerRewardsExtMgt.IsCustomerRewardsActivated, ActivatedTxt);
// [When] User opens Reward Level Page
// [Then] No error
RewardLevelListTestPage.OpenView;
end;
[Test]
procedure TestRewardLevelsActionExistsOnCustomerListPage()
var
CustomerListTestPage: TestPage "Customer List";
begin
// [Scenario] Reward Level action exists on customer list page
// [Given] Customer List Page
CustomerListTestPage.OpenView;
// Using permissions that do not include SUPER
LibraryLowerPermissions.SetO365BusFull;
// [Then] Reward levels action exists on custome list page
Assert.IsTrue(CustomerListTestPage."Reward Levels".Visible, 'Reward Levels action should be visible');
end;
[Test]
[HandlerFunctions('CustomerRewardsWizardModalPageHandler')]
procedure TestRewardLevelsActionOnCustomerListPageOpensCustomerRewardsWizardWhenNotActivated()
var
CustomerRewardsExtMgt: Codeunit "Customer Rewards Ext. Mgt.";
CustomerListTestPage: TestPage "Customer List";
begin
// [Scenario] Reward Levels Action Opens Customer Rewards Wizard When Not Activated
// [Given] Unactivated Customer Rewards
Initialize;
Commit;
// Using permissions that do not include SUPER
LibraryLowerPermissions.SetO365BusFull;
Assert.IsFalse(CustomerRewardsExtMgt.IsCustomerRewardsActivated, NotActivatedTxt);
// [When] User opens Customer List page and invokes action
CustomerListTestPage.OpenView;
CustomerListTestPage."Reward Levels".Invoke;
// [Then] Wizard opens. Caught by CustomerRewardsWizardModalPageHandler
end;
[Test]
[HandlerFunctions('RewardsLevelListlPageHandler')]
procedure TestRewardLevelsActionOnCustomerListPageOpensRewardsLevelListPageWhenActivated()
var
CustomerRewardsExtMgt: Codeunit "Customer Rewards Ext. Mgt.";
CustomerListTestPage: TestPage "Customer List";
begin
// [Scenario] Reward Levels Action Opens Reward Level Page When Activated
// [Given] Activated Customer Rewards
Initialize;
Commit;
// Using permissions that do not include SUPER
LibraryLowerPermissions.SetO365BusFull;
Assert.IsFalse(CustomerRewardsExtMgt.IsCustomerRewardsActivated, NotActivatedTxt);
ActivateCustomerRewards;
Assert.IsTrue(CustomerRewardsExtMgt.IsCustomerRewardsActivated, ActivatedTxt);
// [When] User opens Customer List page and invokes action
CustomerListTestPage.OpenView;
CustomerListTestPage."Reward Levels".Invoke;
// [Then] Wizard opens. Caught by RewardsLevelListlPageHandler
end;
[Test]
procedure TestCustomerCardPageHasRewardsFields()
var
CustomerRewardsExtMgt: Codeunit "Customer Rewards Ext. Mgt.";
CustomerCardTestPage: TestPage "Customer Card";
begin
// [Scenario] Customer Card Page Has Reward Fields When Opened
// [Given] Customer Card Page
// Using permissions that do not include SUPER
LibraryLowerPermissions.SetO365BusFull;
// [When] Customer card page is opened
CustomerCardTestPage.OpenView;
// [Then] Reward fields are exist
Assert.IsTrue(CustomerCardTestPage.RewardLevel.Visible, 'Reward Level should be visible');
Assert.IsFalse(CustomerCardTestPage.RewardLevel.Editable, 'Reward Level should not be editable');
Assert.IsTrue(CustomerCardTestPage.RewardPoints.Visible, 'Reward Points should be visible');
Assert.IsFalse(CustomerCardTestPage.RewardPoints.Editable, 'Reward Points should not be editable');
end;
[Test]
procedure TestNewCustomerHasZeroRewardPointsAndNoRewardLevel()
var
Customer: Record Customer;
CustomerRewardsExtMgt: Codeunit "Customer Rewards Ext. Mgt.";
CustomerCardTestPage: TestPage "Customer Card";
begin
// [Scenario] A new customer Has Zero Reward Points And No Reward Level
// [Given] Activated Customer Rewards
Initialize;
Commit;
// Using permissions that do not include SUPER
LibraryLowerPermissions.SetO365BusFull;
ActivateCustomerRewards;
// [When] New Customer
LibrarySales.CreateCustomer(Customer);
CustomerCardTestPage.OpenView;
CustomerCardTestPage.GoToRecord(Customer);
// [Then] No Reward level
VerifyCustomerRewardLevel(CustomerCardTestPage.RewardLevel.Value, NoLevelTxt);
// [Then] Reward Point is zero
VerifyCustomerRewardPoints(CustomerCardTestPage.RewardPoints.AsInteger, 0);
end;
[Test]
procedure TestCustomerHasCorrectRewardPointsAfterPostedSalesOrders()
var
Customer: Record Customer;
CustomerRewardsExtMgt: Codeunit "Customer Rewards Ext. Mgt.";
CustomerCardTestPage: TestPage "Customer Card";
begin
// [Scenario] Customer Has Correct Reward Points After 4 Posted Sales Orders
// [Given] Activated Customer Rewards and Customer
Initialize;
Commit;
// Using permissions that do not include SUPER
LibraryLowerPermissions.SetO365BusFull;
ActivateCustomerRewards;
// New Customer
LibrarySales.CreateCustomer(Customer);
// [When] 4 Sales Orders
CreateAndPostSalesOrder(Customer."No.");
CreateAndPostSalesOrder(Customer."No.");
CreateAndPostSalesOrder(Customer."No.");
CreateAndPostSalesOrder(Customer."No.");
// [Then] Customer has 4 reward points
CustomerCardTestPage.OpenView;
CustomerCardTestPage.GoToRecord(Customer);
VerifyCustomerRewardPoints(CustomerCardTestPage.RewardPoints.AsInteger, 4);
end;
[Test]
procedure TestCustomerHasNoRewardLevelAfterPostedSalesOrders()
var
Customer: Record Customer;
CustomerRewardsExtMgt: Codeunit "Customer Rewards Ext. Mgt.";
CustomerCardTestPage: TestPage "Customer Card";
begin
// [Scenario] Customer Has 1 Reward Point and No Reward Level After 1 Posted Sales Orders
// [Scenario] Because Lowest Level requires at least 2 points
// [Given] Activated Customer Rewards, Customer, Bronze level for 2 points and above
Initialize;
Commit;
// Using permissions that do not include SUPER
LibraryLowerPermissions.SetO365BusFull;
ActivateCustomerRewards;
AddRewardLevel(BronzeLevelTxt, 2); // 2 points required for BRONZE level
// New Customer
LibrarySales.CreateCustomer(Customer);
CustomerCardTestPage.OpenView;
CustomerCardTestPage.GoToRecord(Customer);
// Verify 0 points and no reward level before sales order
VerifyCustomerRewardPoints(CustomerCardTestPage.RewardPoints.AsInteger, 0);
VerifyCustomerRewardLevel(CustomerCardTestPage.RewardLevel.Value, NoLevelTxt);
// [When] 1 Sales Order
CreateAndPostSalesOrder(Customer."No.");
// [Then] Customer has 1 points and no reward level after sales order
CustomerCardTestPage.GoToRecord(Customer);
VerifyCustomerRewardPoints(CustomerCardTestPage.RewardPoints.AsInteger, 1);
VerifyCustomerRewardLevel(CustomerCardTestPage.RewardLevel.Value, NoLevelTxt);
end;
[Test]
procedure TestCustomerHasBronzeRewardLevelAfterPostedSalesOrders()
var
Customer: Record Customer;
CustomerRewardsExtMgt: Codeunit "Customer Rewards Ext. Mgt.";
CustomerCardTestPage: TestPage "Customer Card";
begin
// [Scenario] Customer Has 2 Reward Points and Bronze Reward Level After 2 Posted Sales Orders
// [Scenario] Because Bronze Level requires at least 2 points
// [Given] Activated Customer Rewards, Customer, Bronze level for 2 points and above
Initialize;
Commit;
// Using permissions that do not include SUPER
LibraryLowerPermissions.SetO365BusFull;
ActivateCustomerRewards;
AddRewardLevel(BronzeLevelTxt, 2); // 2 points required for BRONZE level
// New Customer
LibrarySales.CreateCustomer(Customer);
// [When] 2 Sales Order
CreateAndPostSalesOrder(Customer."No.");
CreateAndPostSalesOrder(Customer."No.");
// [Then] Customer has 2 points and bronze reward level
CustomerCardTestPage.OpenView;
CustomerCardTestPage.GoToRecord(Customer);
VerifyCustomerRewardPoints(CustomerCardTestPage.RewardPoints.AsInteger, 2);
VerifyCustomerRewardLevel(CustomerCardTestPage.RewardLevel.Value, BronzeLevelTxt);
end;
[Test]
procedure TestCustomerHasSilverRewardLevelAfterPostedSalesOrders()
var
Customer: Record Customer;
CustomerRewardsExtMgt: Codeunit "Customer Rewards Ext. Mgt.";
CustomerCardTestPage: TestPage "Customer Card";
begin
// [Scenario] Customer Has 3 Reward Points and Silver Reward Level After 3 Posted Sales Orders
// [Scenario] Because Silver Level requires at least 3 points
// [Given] Activated Customer Rewards, Customer, Bronze level from 2 points, Silver level from 3 points
Initialize;
Commit;
// Using permissions that do not include SUPER
LibraryLowerPermissions.SetO365BusFull;
ActivateCustomerRewards;
AddRewardLevel(BronzeLevelTxt, 2); // 2 points required for BRONZE level
AddRewardLevel(SilverLevelTxt, 3); // 3 points required for SILVER level
// New Customer
LibrarySales.CreateCustomer(Customer);
// 2 Sales Order
CreateAndPostSalesOrder(Customer."No.");
CreateAndPostSalesOrder(Customer."No.");
// Verify 2 points and bronze reward level
CustomerCardTestPage.OpenView;
CustomerCardTestPage.GoToRecord(Customer);
VerifyCustomerRewardPoints(CustomerCardTestPage.RewardPoints.AsInteger, 2);
VerifyCustomerRewardLevel(CustomerCardTestPage.RewardLevel.Value, BronzeLevelTxt);
// [When] 3rd Sales Order
CreateAndPostSalesOrder(Customer."No.");
// [Then] Customer has 3 points and silver reward level
CustomerCardTestPage.GoToRecord(Customer);
VerifyCustomerRewardPoints(CustomerCardTestPage.RewardPoints.AsInteger, 3);
VerifyCustomerRewardLevel(CustomerCardTestPage.RewardLevel.Value, SilverLevelTxt);
end;
[Test]
procedure TestCustomerHasGoldRewardLevelAfterPostedSalesOrders()
var
Customer: Record Customer;
CustomerRewardsExtMgt: Codeunit "Customer Rewards Ext. Mgt.";
CustomerCardTestPage: TestPage "Customer Card";
begin
// [Scenario] Customer Has 4 Reward Points and Gold Reward Level After 4 Posted Sales Orders
// [Scenario] Because Gold Level requires at least 4 points
// [Given] Activated Customer Rewards, Customer
// [Given] Bronze level from 2 points, Silver level from 3 points, Gold level from 4 points
Initialize;
Commit;
// Using permissions that do not inlcude SUPER
LibraryLowerPermissions.SetO365BusFull;
ActivateCustomerRewards;
AddRewardLevel(BronzeLevelTxt, 2); // 2 points required for BRONZE level
AddRewardLevel(SilverLevelTxt, 3); // 3 points required for SILVER level
AddRewardLevel(GoldLevelTxt, 4); // 4 points required for GOLD level
// New Customer
LibrarySales.CreateCustomer(Customer);
// 3 Sales Order
CreateAndPostSalesOrder(Customer."No.");
CreateAndPostSalesOrder(Customer."No.");
CreateAndPostSalesOrder(Customer."No.");
// Verify 3 points and silver reward level
CustomerCardTestPage.OpenView;
CustomerCardTestPage.GoToRecord(Customer);
VerifyCustomerRewardPoints(CustomerCardTestPage.RewardPoints.AsInteger, 3);
VerifyCustomerRewardLevel(CustomerCardTestPage.RewardLevel.Value, SilverLevelTxt);
// [When] 4th Sales Order
CreateAndPostSalesOrder(Customer."No.");
// [Then] Customer has 4 points and gold reward level
CustomerCardTestPage.GoToRecord(Customer);
VerifyCustomerRewardPoints(CustomerCardTestPage.RewardPoints.AsInteger, 4);
VerifyCustomerRewardLevel(CustomerCardTestPage.RewardLevel.Value, GoldLevelTxt);
end;
local procedure OpenCustomerRewardsWizardActivationPage(VAR CustomerRewardsWizardTestPage: TestPage "Customer Rewards Wizard")
begin
CustomerRewardsWizardTestPage.OpenView;
CustomerRewardsWizardTestPage.EnableFeature.SetValue(true);
CustomerRewardsWizardTestPage.ActionNext.Invoke;
end;
local procedure Initialize()
var
ActivationCodeInfo: Record "Activation Code Information";
RewardLevel: Record "Reward Level";
Customer: Record Customer;
begin
Customer.ModifyAll(RewardPoints, 0);
ActivationCodeInfo.DeleteAll;
RewardLevel.DeleteAll;
UnbindSubscription(MockCustomerRewardsExtMgt);
BindSubscription(MockCustomerRewardsExtMgt);
MockCustomerRewardsExtMgt.Setup;
end;
local procedure ActivateCustomerRewards()
var
ActivationCodeInfo: Record "Activation Code Information";
begin
ActivationCodeInfo.Init;
ActivationCodeInfo.ActivationCode := '12345678901234';
ActivationCodeInfo."Date Activated" := Today;
ActivationCodeInfo."Expiration Date" := CALCDATE('<1Y>', Today);
ActivationCodeInfo.Insert;
end;
local procedure CreateAndPostSalesOrder(SellToCustomerNo: Code[20])
var
SalesHeader: Record "Sales Header";
SalesLine: Record "Sales Line";
LibraryRandom: Codeunit "Library - Random";
SalesOrderTestPage: TestPage "Sales Order";
begin
LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Order, SellToCustomerNo);
LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::Item, '', 1);
SalesLine.VALIDATE("Unit Price", LibraryRandom.RandIntInRange(5000, 10000));
SalesLine.MODIFY(TRUE);
LibrarySales.PostSalesDocument(SalesHeader, true, true);
end;
local procedure AddRewardLevel(Level: Text; MinPoints: Integer)
var
RewardLevel: Record "Reward Level";
begin
if RewardLevel.Get(Level) then begin
RewardLevel."Minimum Reward Points" := MinPoints;
RewardLevel.Modify;
end else begin
RewardLevel.Init;
RewardLevel.Level := Level;
RewardLevel."Minimum Reward Points" := MinPoints;
RewardLevel.Insert;
end;
end;
local procedure VerifyCustomerRewardLevel(ExpectedLevel: Text; ActualLevel: Text)
begin
Assert.AreEqual(ExpectedLevel, ActualLevel, 'Reward Level should be the same.');
end;
local procedure VerifyCustomerRewardPoints(ExpectedPoints: Integer; ActualPoints: Integer)
begin
Assert.AreEqual(ExpectedPoints, ActualPoints, 'Reward Points should be the same.');
end;
[ModalPageHandler]
procedure CustomerRewardsWizardModalPageHandler(var CustomerRewardsWizard: TestPage "Customer Rewards Wizard")
begin
end;
[PageHandler]
procedure RewardsLevelListlPageHandler(var RewardsLevelList: TestPage "Rewards Level List")
begin
end;
}
At this point you can publish and run your tests on your tenant by selecting Ctrl+F5.
Run the tests
In order to run the tests, follow these steps:
Open the Test Tool page (130401).
Choose Get Test Codeunits and then choose Select Test Codeunits.
Select your test codeunits and then choose the OK button.
You can now see all the test methods from your test codeunits.
Now, choose Run or Run Selected to run all the tests in the test codeunit or only the selected tests. The Result column indicates whether a test was a SUCCESS or FAILURE. A summary is also presented at the bottom of the page.
Failing tests
Let us look at what to do if you have a failing test. To create a failing test, we'll modify the SetDefaultCustomerRewardsExtMgtCodeunit method in codeunit 50100 Customer Rewards Install Logic to the following:
procedure SetDefaultCustomerRewardsExtMgtCodeunit()
var
CustomerRewardsExtMgtSetup: Record "Customer Rewards Mgt. Setup";
begin
CustomerRewardsExtMgtSetup.DeleteAll;
CustomerRewardsExtMgtSetup.Init;
// Default Customer Rewards Ext. Mgt codeunit to use for handling events
// Changing
// CustomerRewardsExtMgtSetup."Customer Rewards Ext. Mgt. Codeunit ID" := Codeunit::"Customer Rewards Ext. Mgt.";
// To
CustomerRewardsExtMgtSetup."Customer Rewards Ext. Mgt. Codeunit ID" := 0;
CustomerRewardsExtMgtSetup.Insert;
end;
Now, anytime the SetDefaultCustomerRewardsExtMgtCodeunit method in the install codeunit is run, the Customer Rewards Ext. Mgt. Codeunit ID in the Customer Rewards Mgt. Setup table is set to 0.
Select Ctrl+F5 to publish the updated tests to your tenant and then run them.
The test TestOnInstallLogic should now have a Failure result with the error message:
"Assert.AreEqual failed. Expected:<50101> (Integer). Actual:<0> (Integer). Codeunit does not match default."
The error message shows that the actual result in one of our Assert statements differed from what was expected. According to the error message, the Assert statement was expecting a value of 50101 but actually got a value of 0. We can also tell where in our code this is happening because of the message; "Codeunit doesn't match default", which we defined earlier when we wrote our tests. If we had no idea where the error occurred, we can select the error message to open the Test Results page and then choose the Call Stack action.
Choosing the Call Stack action gives you a message alert that contains an ordered list of method calls up to the one that caused the error.
The list of method calls is arranged from the most recent at the top to the oldest at the bottom. In our example, we can tell that the Assert(CodeUnit 130000).AreEqual
(the first on the list) was the last method to be run, indicating where the error was found. Because we didn't modify the Assert codeunit, then the wrong values or results must have been passed to it. The next item on the list, "Customer Rewards Test"(CodeUnit 50103).TestOnInstallLogic_Scope_1248196953
line 35 points to the method that was run before the final one that caused the error. This time, it is in the TestOnInstallLogic method of codeunit 50103 Customer Rewards Test after line 35.
On line 36 of codeunit 50103 Customer Rewards Test, we can see the Assert statement that throws the error. We tested that the result should be Codeunit::"Customer Rewards Ext. Mgt."
, which is 50101, when our install logic is run, however, the result of the test indicated that we got a result of 0. This implies that our install logic isn't working as expected. To fix this, we need to examine all the previous lines of code in the method to figure out where we went wrong. This leads us to line 31, where the SetDefaultCustomerRewardsExtMgtCodeunit method call is made.
When you go into the SetDefaultCustomerRewardsExtMgtCodeunit method, codeunit 50100 Customer Rewards Install Logic, you will see the change, we made to cause the test to fail. Revert it so that CustomerRewardsExtMgtSetup."Customer Rewards Ext. Mgt. Codeunit ID"
now stores Codeunit::"Customer Rewards Ext. Mgt."
, instead of 0. Publish the updated extension and tests to your tenant and run the tests again. The test TestOnInstallLogic should pass now because the actual result matches what is expected.
Conclusion
At this point, the Customer Rewards sample extension can be published and installed on your sandbox.
See Also
Developing Extensions
Get Started with AL
How to: Publish and Install an Extension
Converting Extensions V1 to Extensions V2