Skip to the content

News

From fresh to shared fixture #3 – refactoring can start

At the end of the of this series I left you hanging just when I was ready to really start refactoring. Sorry for that and shouldn’t let you no longer. So, here we go.

Refactoring can start

But where to start? Well, let’s list the things that could be cleaned up. In this I use the following rules of thumb:

  1. If data creation helper functions are referenced multiple times from different tests, these might be good candidates to be moved in to the Initialize function. In this way we promote fresh fixture  into shared fixture.
    Note that data creator helper functions are created based on the [GIVEN] clauses
  2. If different helper functions contain almost the same code, these are candidates for generalization: from a number of specific methods move the code into one generic method.

In the context of this blog post series we’ll focus on my first rule of thumb only.

So, what data creation helper methods in my test codeunit are referenced multiple times?

Have a look:

Having 5 tests coded and having two of the data creation helper functions referenced 5 times it’s quite obvious that these are called by each test function (and which we can easily check in the code). Sounds like real good candidates for a promotion to the Initialize function. If the concept of the Initialize function does not sound familiar to you read more about it in my book or in the .

// [GIVEN] Location with require shipment

I am first going to focus on the helper function CreateLocationWithRequireShipment for the very reason that it is applied in each of the 5 test functions in exactly the same way:

// [GIVEN] Location with require shipment
LocationCode := CreateLocationWithRequireShipment();

With LocationCode being a local variable of type Code[10].

Moving this statement into Initialize means that not each test will create a new location but instead will only be created once. This is not only has a refactoring result – elimination of duplication, or clean-up – but it also makes running all 5 tests more efficient as the location only needs to be created once; as we will see below.

Keep in mind: taking small steps. Let’s not refactor all 5 tests in one go but one by one running the tests after each (small) change.

Before – [SCENARIO #0001]

[Test]
procedure DeleteByUserWithNoAllowanceManuallyCreatedWhseShptLine()
// [FEATURE] Unblock Deletion of Whse. Shpt. Line enabled
var
    LocationCode: Code[10];
    ...
begin
    // [SCENARIO #0001] Delete by user with no allowance manually created whse. shpt. line
    Initialize();

    ...
    // [GIVEN] Location with require shipment
    LocationCode := CreateLocationWithRequireShipment();
    ...
end;

The three dots (…) are used to replace other codes parts that are not in focus right now.

After – [SCENARIO #0001]

[Test]
procedure DeleteByUserWithNoAllowanceManuallyCreatedWhseShptLine()
// [FEATURE] Unblock Deletion of Whse. Shpt. Line enabled
var
    ...
begin
    // [SCENARIO #0001] Delete by user with no allowance manually created whse. shpt. line
    // [GIVEN] Location with require shipment
    Initialize();

    ...
end;

Before – Initialize

local procedure Initialize()
begin
    if IsInitialized then
        exit;

    IsInitialized := true;
    Commit();
end;

The make the focus on our work here better I have simplified the setup of Initialize here compared to the one you will find in the GitHub repo.

After – Initialize

var
    LocationCode: Code[10];

local procedure Initialize()
begin
    if IsInitialized then
        exit;

    // [GIVEN] Location with require shipment
    LocationCode := CreateLocationWithRequireShipment();

    IsInitialized := true;
    Commit();
end;

Now that LocationCode is assigned a value in Initialize and is used in the first test function, it needs to be declared as a global variable as can been seen above.

Exercising the same refactoring step for the other four tests – refactoring and retesting one by one – will give the following final test run result:

Notes

  • Refactoring any of the next test functions is with a smaller step than for the first test function as I do not need to update the Initialize function.
  • After each rerun the duration of that test diminishes… except for the first test, because with that test Initialize is fully executed.
  • Running each test individually will make it duration again just as long as before, because in the case the location needs to be created.
  • We have to realize that we can “promote” CreateLocationWithRequireShipment due to the fact that the location is not consumed, being that, it indeed is reusable in all those five tests.

// [GIVEN] Enable “Unblock Deletion of Shpt. Line” on warehouse setup

What next? Well, the other data creator EnableUnblockDeletionOfShptLineOnWarehouseSetup.This is called in exactly the same way in the first four tests. These have all a similar setup/purpose, so, we could most probably move this into the Initialize function.

Before – [SCENARIO #0001]

[Test]
procedure DeleteByUserWithNoAllowanceManuallyCreatedWhseShptLine()
// [FEATURE] Unblock Deletion of Whse. Shpt. Line enabled
var
    ...
begin
    // [SCENARIO #0001] Delete by user with no allowance manually created whse. shpt. line
    // [GIVEN] Location with require shipment
    Initialize();

    ...
    // [GIVEN] Enable "Unblock Deletion of Shpt. Line" on warehouse setup
    EnableUnblockDeletionOfShptLineOnWarehouseSetup();
    ...
end;

The three dots (…) are used to replace other codes parts that are not in focus right now.

After – [SCENARIO #0001]

[Test]
procedure DeleteByUserWithNoAllowanceManuallyCreatedWhseShptLine()
// [FEATURE] Unblock Deletion of Whse. Shpt. Line enabled
var
    ...
begin
    // [SCENARIO #0001] Delete by user with no allowance manually created whse. shpt. line
    // [GIVEN] Enable "Unblock Deletion of Shpt. Line" on warehouse setup
    // [GIVEN] Location with require shipment
    Initialize();

    ...
end;

Before – Initialize

local procedure Initialize()
begin
    if IsInitialized then
        exit;

    // [GIVEN] Location with require shipment
    LocationCode := CreateLocationWithRequireShipment();

    IsInitialized := true;
    Commit();
end;

Note that the setup of Initialize here is somewhat simpler than you will find in the GitHub repo.

After – Initialize

local procedure Initialize()
begin
    if IsInitialized then
        exit;

    // [GIVEN] Enable "Unblock Deletion of Shpt. Line" on warehouse setup
    EnableUnblockDeletionOfShptLineOnWarehouseSetup();
    // [GIVEN] Location with require shipment
    LocationCode := CreateLocationWithRequireShipment();

    IsInitialized := true;
    Commit();
end;

But what about the fifth test in which EnableUnblockDeletionOfShptLineOnWarehouseSetup is not used in the [GIVEN] section, rather in the [WHEN]? If we would add this helper function to Initialize will it be triggered uselessly? As matter of fact, not really, as it is already triggered in each preceding test anyway.

Let’s see what the overall effect is on the test duration:

Notes

  • Having EnableUnblockDeletionOfShptLineOnWarehouseSetup executed uselessly
  • Of course, as we all know each process, like a test run in our case, is not executing in exactly the same duration each time. The above screenshot is just one example of a test run.
  • The duration shortening due to the promotion of EnableUnblockDeletionOfShptLineOnWarehouseSetup to Initialize is nothing compared to the promotion of CreateLocationWithRequireShipment .

How Do I: Create a simple API query and page for PowerBI?

In this post I will demonstrate how you can create simple APIs of type Query and Page to be used in Power BI . The idea is to have a look at an example where we create 2 very simple APIs: a Customers Query and a Customers Page . In VSCode, create a...

Webinar | 8 March | Women in Dynamics Launch with Gavriella Schuster

After the hugely successful soft launch of Women in Dynamics during Directions EMEA in Milan, we are delighted to welcome Gavriella Schuster as our special guest for the official launch event on the 8th of March.   20 years ago, only 36% of the computing workforce was women. One would expect that things have improved today but unfortunately, we’ve seen a steady decline to 25 ...

Why you should join DynamicsCon March 2022?

DynamicsCon March 2022 is a FREE , 3 day virtual learning experience for 8,500+ Microsoft Business Application users and professionals. You will be immersed in programming covering Power Platform , Dynamics GP , and Dynamics 365 products: Business Central...

From fresh to shared fixture #2 – a refactoring example

In the of this series I started with stating the intention that …

I would like to display a very simple, though instructive, example of how easy refactoring works when you have both app and test code at your disposal.

And that’s what I am going to do in this second post as I had to “elaborate on a number of concepts that I will be using” first in the previous post.

The example I am going to display is one I often make use of in my TA courses and workshops: that you can find in the GitHub repo and consist out of two sub features, or so-called ATDD features:

  • Unblock Deletion of Whse. Shpt. Line enabled
  • Unblock Deletion of Whse. Shpt. Line disabled

In this example I focus on the test automation part only, so, I am not going down the TDD path where test and app code evolve in sync. Here app code is already in place and test code still needs to be created. Note that whatever road I follow my test coding is always based on a test plan and test design (see chapter 5 of ), aka as ATDD sheet, that defines the scenarios my tests should be verifying.

Test Plan and Test Design

This is how I describe a test plan in my book:

A test plan should list all possible tests that are needed to verify whether the application, or just a singular feature, is doing what it is expected to do. First of all, it should entail both the sunny scenarios, a.k.a. positive tests and often defined as user stories or use cases, and rainy scenarios, a.k.a. positive-negative tests, that is, tests that check that exceptions are handled.

For the functionality the test plan in ATDD format would be:

[FEATURE] Unblock Deletion of Whse. Shpt. Line enabled
[SCENARIO #0002] Delete by user with no allowance automatically created whse. shpt. line
[SCENARIO #0003] Delete by user with allowance manually created whse. shpt. line
[SCENARIO #0004] Delete  by user with allowance automatically created whse. shpt. line
[SCENARIO #0009] “Allowed to Delete Shpt. Line” is not editable on warehouse employees page

[FEATURE] Unblock Deletion of Whse. Shpt. Line disabled
[SCENARIO #0005] Delete  by user with no allowance manually created whse. shpt. line
[SCENARIO #0006] Delete by user with no allowance automatically created whse. shpt. line
[SCENARIO #0007] Delete by user with allowance manually created whse. shpt. line
[SCENARIO #0008] Delete  by user with allowance automatically created whse. shpt. Line with confirmation
[SCENARIO #0011] Delete by user with allowance automatically created whse. shpt. line with no confirmation
[SCENARIO #0010] “Allowed to Delete Shpt. Line” is editable on warehouse employees page

Now fully applying the ATDD pattern detailing out our scenarios with the GIVENWHENTHEN parts, transforms our test plan into a test design. Leaving to you to study the full test design – the ATDD sheet – you can find , this would be the test design for the first scenario:

[FEATURE] Unblock Deletion of Whse. Shpt. Line enabled
[SCENARIO #0001] Delete by user with no allowance manually created whse. shpt. line
[GIVEN] Enable “Unblock Deletion of Shpt. Line” on warehouse setup
[GIVEN] Location with require shipment
[GIVEN] Warehouse employee for current user with no allowance
[GIVEN] Manually created warehouse shipment from released sales order with one line with require shipment location
[WHEN] Delete warehouse shipment line
[THEN] Warehouse shipment line is deleted

Test Coding

OK, now, given the (full) test design, I am set to work on my first ATDD feature and get scenario #0001 coded. Once completed, that is, running successful, I proceed to #0002, #0003, #0004 and #0009. In this flow of getting one scenario coded after the other, I might pay attention to rule 2 of the TDD game (see the of this series), but I very often do not want or have to for the following reasons (in random order):

  1. I adhere as much as possible to the ATDD attitude of “getting the tests passing asap” and not allowing me a lot of time to refactor.
  2. Using the and given a consistent description of the various ATDD tags, the test code setup generated often already has created reusable helper functions.

This leads to a first complete version of coded tests for the first feature:

[FEATURE] Unblock Deletion of Whse. Shpt. Line enabled

As this test codeunit entails all together 270 lines of code it is a bit too much to include it fully in this post. Therefore a reference to it: . And of course have some kind of proof that these indeed are successful tests:

Given app and test code, that seemingly are in balance as the test are passing, I am set to finally start on the goal of this blog post series: refactoring. Cleaning up the code to remove duplication and transform the specific into generic. Doing this I try to keep in mind that each change is done in small enough steps – another ATDD attitude – and is always verified by rerunning the tests. Any failure in any of the tests after a change cannot but have its root in this change.

Refactoring can start

Ouch, yes, sorry for keeping you in suspense with another round of introductory stuff. Badly planned, I must admit, but I nevertheless leave the real refactoring for a next post that will follow very soon.

Microsoft Dynamics 365: 2022 Release Wave 1 plan (Business Central)

The “2022 Release Wave 1” plans are already out there for quite a while – Erik Hougaard already did his video about it – and there are already blogs out there about it – so it’s about time that I do that as well (as I have been doing that for the previous major releases …

Did you know that the Business Central connector in Power BI supports related tables when using APIs?

Recently the Power BI connector in Power BI Desktop allows you to harvest the power of Business Central APIs : In the Standard APIs folder you can see all APIs Business Central publishes automatically: API(V2.0) for Dynamics 365 Business Central...

How do I: Find the correct version of Visual Studio to design Business Central RDLC report layouts?

A question I receive a lot recently is that after installing Visual Studio, the installation of the RDLC Report Designer produces an error: The install of Microsoft RDLC Report Designer was not successful for all the selected products. This extension...

Quick Tip: New landing page for all things related to reporting and data analysis in Business Central!

Introducing the new landing page for all things related to reporting and data analysis in Business Central: http://aka.ms/bcreporting Similar to landing pages for security ( http://aka.ms/bcsecurity ), telemetry ( http://aka.ms/bctelemetry ), and...

What’s planned for Business Central 2022 Wave1

What’s planned for Business Central 2022 Wave1 The features that will be released in Business Central 2022 Wave1 have been