top of page

What are we testing, exactly?

Planning your test scenarios

Let's get started with the first test example.

I find it a good introductory exercise to pick a very simple procedure or a trigger from the Business Central BaseApp and cover it with unit tests. This is a very basic task from the development perspective, and it helps to focus on very fundamental testing principles without dispersing attention on coding complexities.

In the following few code snippets, I will follow the same practice, choosing a sample AL trigger and writing unit tests for it, explaining the logic and decisions as we go. For this exercise, I selected the table Currency and the trigger OnValidate of the field "Max. VAT Difference Allowed".


First question a developer, who starts planning test coverage, should answer is "what do I want to test?" The most common and obvious answer "OnValidate trigger" is, of course, correct, but it does not bring you any closer to the solution. It is similar to someone saying "winter" to answer a question "What time is it?" Probably this is a very accurate answer, but one that does not help at all. So where should we start then?


Start from questioning your code

No, I'm not kidding now. We are talking about unit testing following the white box technique. In the following posts, I will cover differences between various types of tests and white box vs. black box testing, but now let's just say that white box tests are centred around the code being tested. Therefore, all the answers sit there, in the code, you just need to ask right questions. I suggest two principles which can help in finding the proper questions and, ultimately, guide you to the correct answers.

The first principle is:


Any test has value as long as it can fail

Every test is performed to verify that something - piece of code, mobile phone, car, or a spaceship - satisfies expectations. If the code under test does not work as expected, there must be a failing test. And vice versa - a test aimed on a specific code statement must fail if the code under test changes and ceases to satisfy the requirements.

The second principle declares:


The purpose of a regression test is to protect the code under test from unwanted changes.

Every line of code has a meaning, and each statement is another small step leading to the final goal of coding - working application. And test cases must become vigilant guides directing the code flow towards the declared goal, making sure that the application execution does not go astray.

On the other hand, various ways can lead to the same goal, just like all roads, always leading to Rome. Code can be designed and written in different ways, and still all variations will be solving the same task. Therefore, the guides must not turn into prison wardens, locking the code in cells. Test cases must protect the code from changes which introduce bugs and disrupt functionality, at the same time allowing space for refactoring - changes neutral from the functional perspective, but important from the technical point of view.

Armed with these two testing principles, let's take a look at the code snippet which we are going to test and try to build our first test case.

Here is the code to be tested.

field(52; "Max. VAT Difference Allowed"; Decimal)
{
    AutoFormatExpression = Code;
    AutoFormatType = 1;
    Caption = 'Max. VAT Difference Allowed';

    trigger OnValidate()
    begin
        if "Max. VAT Difference Allowed" <> Round("Max. VAT Difference Allowed", "Amount Rounding Precision") then
            Error(
                Text001,
                FieldCaption("Max. VAT Difference Allowed"), "Amount Rounding Precision");

        "Max. VAT Difference Allowed" := Abs("Max. VAT Difference Allowed");
    end;
}

From the very first lines of the validation trigger, we can see that the validation trigger controls the value assigned to the field, verifying that the number of decimal symbols matches the same in the amount rounding precision for this currency, and this where we should aim our first test. A short description of the test scenario should highlight the connection between the maximum VAT difference and the amount rounding precision. Like this, for example:

Validation of "Max. VAT Difference Allowed" fails if the number of decimals does not match the amount rounding precision

And the test case will be aimed on assigning an amount which will trigger the error.

[Test]
procedure ValidateMaxVatDifferenceWithMismatchingPrecisionFails()
var
    Currency: Record Currency;
    MustBeRoundedErr: Label '%1 must be rounded to the nearest %2';
begin
    // [SCENARIO] Validation of "Max. VAT Difference Allowed" fails if the number of decimals does not match the amount rounding precision

    // [GIVEN] Currency "C" with "Amount Rounding Precision" = 0.01
    CreateCurrencyWithAmountRoundingPrecision(Currency, 0.01);

    // [WHEN] Set "Max. VAT Difference Allowed" = 0.019 on the currency
    asserterror Currency.Validate("Max. VAT Difference Allowed", 0.019);

    // [THEN] Validation fails with the error "Max VAT. Difference allowed must be rounded to the nearest 0.01"
    Assert.ExpectedError(
        StrSubstNo(
            MustBeRoundedErr, Currency.FieldCaption("Max. VAT Difference Allowed"), Currency."Amount Rounding Precision"));
end;

This test tries sets the value of the amount rounding precision to 0.01, and after that tries to assign 0.019 as the max VAT difference. This assignment triggers an error because 0.019 rounded to two decimals is not equal to the same value before rounding. This is what is called a negative test - test that verifies reaction of the code to incorrect inputs and expects an error. The execution part of a negative test is usually run under the asserterror statement, and the verification part ensures that the error message matches the expectation.

A positive test, on the contrary, follows the direct execution path, providing correct inputs which must not raise an error. A positive test for the max VAT difference amount can be stated as Validation of "Max. VAT Difference Allowed" succeeds if the number of decimals does matches the amount rounding precision.


[Test]
procedure ValidateMaxVatDifferenceMatchingPrecisionSucceeds()
var
    Currency: Record Currency;
    Precision: Decimal;
begin
    // [SCENARIO] Validation of "Max. VAT Difference Allowed" succeeds if the number of decimals does matches the amount rounding precision

    // [GIVEN] Currency "C" with "Amount Rounding Precision" = 0.01
    Precision := 0.02;
    CreateCurrencyWithAmountRoundingPrecision(Currency, Precision);

    // [WHEN] Set "Max. VAT Difference Allowed" = 0.02 on the currency
    Currency.Validate("Max. VAT Difference Allowed", Precision);

    // [THEN] Value is successfully updated to 0.02
    Assert.AreEqual(
        Precision, Currency."Max. VAT Difference Allowed",
        StrSubstNo(UnexpectedFieldValueErr, Currency.TableCaption, Currency.FieldCaption("Max. VAT Difference Allowed")));
end;

This is an example of a positive test which initialises the rounding precision to 0.01 and sets the allowed VAT difference to 0.02 which successfully passes the validation after rounding to two decimals.

Finally, the last statement of the trigger substitues the value passed to it with its absolute value

"Max. VAT Difference Allowed" := Abs("Max. VAT Difference Allowed");

A test aimed on the verification of this line can simply pass a negative amount to the validation trigger and verify that the resulting value of the field is positive.

[Test]
procedure ValidateNegativeMaxVatDifferencePositiveValueAssigned()
var
    Currency: Record Currency;
    Precision: Decimal;
begin
    // [SCENARIO] Validation of "Max VAT Difference Allowed" replaces a negative amount with its absolute value

    // [GIVEN] Currency "C" with "Amount Rounding Precision" = 0.01
    Precision := 0.01;
    CreateCurrencyWithAmountRoundingPrecision(Currency, Precision);

    // [WHEN] Set "Max. VAT Difference Allowed" = -0.01 on the currency
    Currency.Validate("Max. VAT Difference Allowed", -Precision);

    // [THEN] "Max. VAT Difference Allowed" on the currency "C" is 0.01
    Assert.AreEqual(
        Precision, Currency."Max. VAT Difference Allowed",
        StrSubstNo(UnexpectedFieldValueErr, Currency.TableCaption, Currency.FieldCaption("Max. VAT Difference Allowed")));
end;

These three test cases suggested above are sufficient to cover the OnValidate trigger of the field "Max. VAT Difference Allowed" and protect if from bugs crawling into the code. The last bit that remains uncovered so far is the procedure CreateCurrencyWithAmountRoundingPrecision that creates a test currency with the given amount rounding precision. Below is the procedure code.


local procedure CreateCurrencyWithAmountRoundingPrecision(
    var Currency: Record Currency; Precision: Decimal)
begin
    LibraryERM.CreateCurrency(Currency);
    Currency.Validate("Amount Rounding Precision", Precision);
    Currency.Modify(true);
end;

And, for completeness, the global variables used in tests.


var
    LibraryERM: Codeunit "Library - ERM";
    Assert: Codeunit Assert;
    UnexpectedFieldValueErr: Label 'Unexpected value of %1.%2', Comment = '%1 = Table caption, %2 = Field caption';


219 views2 comments

Recent Posts

See All

Positive value of a negative test

A QA engineer walks into a bar. Orders a beer. Orders 0 beers. Orders 99999999999 beers. Orders a lizard. Orders -1 beers. Orders a...

2 Comments


mrmubashiryounas
Mar 01, 2022

Thanks for your effort and knowledge sharing, but I have a question, If we are going to cover each condition and line of code then the automation testing code will be greater then the actual code. My question is that, how can we limit testing scope to meet the deadlines and stay in budget as well.

Like
adrogin
Mar 07, 2022
Replying to

You know how to ask tough questions. :-)

The proper answer to this question is a matter of many tradeoffs between the quality and thoroughness of the test coverage, development budget, maintenance costs (we should not forget that tests require maintenance too), etc. The test-driven development paradigm assumes that no production code can be written until there is a failing test for this code. If we follow this approach, we will have a test case for each conditional statement and each line of the code. These unit tests are light-weight and easily produced, but in the end the volume of test code can surely exceed the volume of the production code.

In practice though, development teams assume a certain percentag…

Like
bottom of page