The third post of the series of typical testing mistakes will be dedicated to fixing these mistakes. In two previous posts, I was describing a contrived example of a table extension with some simple application logic around it and possible ways to write tests which in fact, consistently fail to verify any functionality. In this post, I will be looking at the same functionality under a different angle, developing proper test coverage that can ensure the quality of the solution.
But before I begin constructing test coverage for the sample extension, let me take a step back and remind you (and myself) what the code that we want to test looks like.
Below is the code snippet which I am going to cover in this post. Complete example of the extension along with the test codeunit is available on GitHub: https://github.com/adrogin/CustomerBillingExtension
tableextension 69100 "TD Customer Billing" extends Customer
{
fields
{
field(50000; "TD Billing Period Date Calc."; DateFormula)
{
Caption = 'Billing Period Date Calc.';
DataClassification = CustomerContent;
}
field(50001; "TD Billing Group Code"; Code[20])
{
Caption = 'Customer Billing Group';
DataClassification = CustomerContent;
TableRelation = "TD Customer Billing Group";
trigger OnValidate()
begin
if Rec."TD Billing Group Code" = '' then
ResetBillingParams()
else
AssignBillingParams("TD Billing Group Code");
end;
}
}
local procedure ResetBillingParams()
var
EmptyDateFormula: DateFormula;
begin
Evaluate(EmptyDateFormula, '');
Rec.Validate(Priority, 0);
Rec.Validate("TD Billing Period Date Calc.", EmptyDateFormula);
end;
local procedure AssignBillingParams(BillingGroupCode: Code[20])
var
CustomerBillingGroup: Record "TD Customer Billing Group";
begin
CustomerBillingGroup.Get(BillingGroupCode);
Rec.Validate(Priority, CustomerBillingGroup.Priority);
Rec.Validate(
"TD Billing Period Date Calc.",
CustomerBillingGroup."Billing Period");
end;
}
In this series of posts, I am writing about "white box" testing, which is a testing technique based on the analysis of the source code. We are pursuing the goal of developing test cases sufficient to ensure the quality of the application, examining the code itself. As it has been discussed previously, this approach is based on breaking down the functionality under test to basic scenarios, its low-level primitives, and defining the success and failure outcomes for each scenario. I wrote on this approach in the post "What are we testing, exactly" here:
The sample table extension which we intend to test contains executable code in a single validation trigger in the field "TD Billing Group Code", and apparently this is where our testing effort should be directed. The trigger itself is obviously split in two separate parts, each calling one of the two local functions. Let's have a closer look at the first part:
if Rec."TD Billing Group Code" = '' then
ResetBillingParams()
Translated from a computer programming language into plain English, these two lines read "if the billing group code in custom record is blank then reset billing parameters". The condition of this statement is more or less clear, so we move to the execution part, which is resetting the billing parameters.
local procedure ResetBillingParams()
var
EmptyDateFormula: DateFormula;
begin
Evaluate(EmptyDateFormula, '');
Rec.Validate(Priority, 0);
Rec.Validate("TD Billing Period Date Calc.", EmptyDateFormula);
end;
Looking at the procedure ResetBillingParams, we can see that "resetting" here means:
assigning 0 to the priority field
assigning the empty date formula to the billing period calculation
And here it is, our definition of the successful execution of the first basic scenario:
[WHEN] Set the billing group code on the customer record to blank
[THEN] Priority is reset to 0
[THEN] "Billing Period Date Calc." is reset to the empty formula
This looks like a test! But let's take a step back and remember the key question we want to ask to every test which we produce. How can we make this test fail? We defined the successful execution for the test, but what is the definition of a failure? Well, this seems quite intuitive - failure is anything that is not success. Apparently the test should be defined as failed if "THEN" conditions are not satisfied after executing the "WHEN" action. Meaning that either the Priority field is not 0, or the billing period formula is not blank after assigning the empty code to the billing group.
But let's change the perspective just slightly and recall that we don't need a test which always demonstrates 100% success rate, we need a test which fails when unexpected changes are introduced in the application code. Now we can take the next logical step in this train of thought and ask ourselves if the scenario described above is sufficient to fail the test in case of unwanted changes? Probably not, for a simple reason: if the value of the Priority in the customer card is 0 at the beginning of the test, the test will not be able to capture an error if the validation trigger suddenly stops updating the field. If the field value was 0 before the test execution, it will still be 0 even if someone completely deletes the validation trigger. So probably we want to add another initialisation step before executing the action under test:
[GIVEN] Create a customer record
[GIVEN] Set Priority = 1, "Billing Period Date Calc." = "5D" on the customer
[WHEN] Set the billing group code on the customer record to blank
[THEN] Priority is reset to 0
[THEN] "Billing Period Date Calc." is reset to the empty formula
With this precondition, we can be sure that a situation of the trigger being deleted will not go unnoticed.
And finally, here the full test verifying the first part of the validation - resetting of the billing parameters on entering the blank group code.
[Test]
procedure ValidateEmptyBillingGroupCodeParametersReset()
var
Customer: Record Customer;
EmptyDateFormula: DateFormula;
WrongBillingParamsErr:
Label 'Customer billing parameters must be reset';
begin
// [SCENARIO] Billing parameters on the customer card are reset to defaults after assigning a blank group code
// [GIVEN] Set Priority = 1, "Billing Period Date Calc." = 5D on a customer card
LibrarySales.CreateCustomer(Customer);
Customer.Validate(Priority, LibraryRandom.RandInt(10));
Evaluate(
Customer."TD Billing Period Date Calc.",
StrSubstNo(PeriodFormulaTxt, LibraryRandom.RandInt(10)));
Customer.Modify(true);
// [WHEN] Set a blank billing group code on the customer card
Customer.Validate("TD Billing Group Code", '');
// [THEN] Priority is reset to 0
Assert.AreEqual(0, Customer.Priority, WrongBillingParamsErr);
// [THEN] Date formula is reset to empty value
Evaluate(EmptyDateFormula, '');
Assert.AreEqual(
EmptyDateFormula, Customer."TD Billing Period Date Calc.",
WrongBillingParamsErr);
end;
Having completed the first test, we can apply the same logic to the rest of the billing group validation, the code branch which expects a non-empty group code and pulls the customer billing parameters from the preconfigured group to the customer card. In the new test, we still want to verify the validation code of the "Billing Group Code" field. What changes though, and what makes this test different is the precondition - the billing group code must be not blank to steer the execution on a different path. And this changes the action under test, the "WHEN" part of the test case. Now we will be assigning a group code instead of the empty string.
[GIVEN] Create a customer record
[GIVEN] Create a customer billing group "G" with Priority = 2 and "Billing Period Date Calc." = "5D"
[GIVEN] Set Priority = 1, "Billing Period Date Calc." = "5D" on the customer
[WHEN] Set "Billing Group Code" = "G" on the customer record
[THEN] Priority = 1 on the customer record
[THEN] "Billing Period Date Calc." = "5D" on the customer record
Once the scenario is ready, translating it to code is not rocket science.
[Test]
procedure ValidateBillingGroupCodeParametersInheritedFromGroup()
var
Customer: Record Customer;
CustomerBillingGroup: Record "TD Customer Billing Group";
WrongBillingParamsErr:
Label 'Customer billing parameters must be inherited from the billing group';
begin
// [SCENARIO] When a billing group is updated on a customer card, billing parameters are copied from the group to the customer
// [GIVEN] Customer "C" with no billing group, Priority = 0, "Billing Period Date Calc." is blank
LibrarySales.CreateCustomer(Customer);
// [GIVEN] Create customer billing group "G" with Priority = 2 and billing period formula "5D"
CreateCustomerBillingGroup(CustomerBillingGroup);
// [WHEN] Set the billing group code "G" on the customer card "C"
Customer.Validate("TD Billing Group Code", CustomerBillingGroup.Code);
// [THEN] Billing fields are updated on the customer "C": Priority = 2, "Billing Period Date Calc." = "5D"
Assert.AreEqual(
CustomerBillingGroup.Priority, Customer.Priority,
WrongBillingParamsErr);
Assert.AreEqual(
CustomerBillingGroup."Billing Period",
Customer."TD Billing Period Date Calc.", WrongBillingParamsErr);
end;
The procedure CreateCustomerBillingGroup that creates and initialises the billing group for the test is also here.
local procedure CreateCustomerBillingGroup(
var CustomerBillingGroup: Record "TD Customer Billing Group")
begin
CustomerBillingGroup.Validate(Code, LibraryUtility.GenerateGUID());
CustomerBillingGroup.Validate(Priority, LibraryRandom.RandInt(10));
Evaluate(
CustomerBillingGroup."Billing Period",
StrSubstNo(PeriodFormulaTxt, LibraryRandom.RandInt(10)));
CustomerBillingGroup.Insert(true);
end;
To conclude, I want to emphasize once again that a developer who starts writing a test case must, first and foremost, have a clear understanding of the scenario under test and unambiguous definitions of success and failure for the test case. "Given the condition A when the action B is executed, the value of the field X is equal Y". If you don't have this precise sequence in your mind, there is little chance of building a good meaningful test. Forget about coding for a while, write the test scenario down in plain text, draw a diagram or a comic strip on the www.storyboardthat.com. Do anything that can help you connect the three fundamental parts on any test: setup (GIVEN), execution (WHEN), verification (THEN). And then, equipped with this understanding, return to coding which will be made much easier.
Comments