Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

This article discusses the use of unit tests in Test Driven Development for Consulting (TDD4C). It explains why and when to write unit tests and provides samples of unit tests for different scenarios. The article also covers how to test element results, test functions within elements, and more complex test definitions. It includes examples of mocking API functions, mocking the "api.stream" function, mocking other element results, mocking API variables, mocking functions from shared libraries, and testing functions that modify tables and always return null. Overall, the article aims to provide guidance on writing effective unit tests in TDD4C.

...

Code Block
languagegroovy
..
.. Some element code not important for this sample...
..

def getRetailerSIPrice(retailerSIRecords) {
    retailerSIRecords = retailerSIRecords.findAll { it.RetailerSIPriceEUR != null }		// Remove all null values in RetailerSIPriceEUR
    def retailerSIPrice

    if (retailerSIRecords) {
        retailerSIPrice = retailerSIRecords?.RetailerSIPriceEUR?.sum() / retailerSIRecords?.size()
    } else {
        retailerSIPrice = null
    }

    return retailerSIPrice
}
Infotip

CODE REVIEW: The function getRetailerSIPrice takes an argument retailerSIRecords, which is expected to be a collection of records. Here's a breakdown of what this function does:

  1. It filters the retailerSIRecords to include only those records where RetailerSIPriceEUR is not null.

  2. It declares a variable retailerSIPrice without initializing it.

  3. It checks if retailerSIRecords is not empty:

    • If not empty, it calculates the sum of RetailerSIPriceEUR across all records and divides it by the number of records (average price), then assigns this value to retailerSIPrice.

    • If empty, it sets retailerSIPrice to null.

  4. Finally, it returns the value of retailerSIPrice.

The purpose of this function is to calculate the average Retailer Selling Price in Euros (if available) from a collection of retailer sales records.

...

Info

NOTE: Function return calculated result for this sample == 11.5.

Complex Test Definitions

The test definitions cover various This section covers various comples scenarios, including mocking API functions such as findLookupTable or find, mocking the "stream," mocking the result of another element, mocking api.global or api.local variables, mocking functions from a shared library, testing a function that modifies local or global tables and always returns null, and testing a function using a function from another element.

This section contains these following sample test definitiondefinitions:

  • Mock api function, e.g. findLookupTable or find.

  • Mock “stream

  • Mock another element result

  • Mock api.global or api.local variables

  • Mock function from (shared) Library

  • Test function if it modifies local or global table and always returns null

  • Test function using function from another element

...

Test Function with Local or Global table

Let’s have a element In this section, we discuss testing a function that modifies local / or global table tables and always returns null. The questions is - how to test such element if return value is always the same (null)? Well, it is not that hard as you may think.So let’s have element code that just add value to local map. Most important lines are 26 where map value is added and 29 where null is always It involves creating a test case to verify the expected behavior when the function returns null, as well as checking if the function modifies the local or global table as intended. This can be accomplished by setting up the necessary input parameters and mock data for the function and then asserting that the returned result matches the expected outcome.

...

Overview of Code

Testing an element that modifies local or global tables and always returns null may seem challenging, but it is not as difficult as it appears. So, let’s review some code that just adds a value to local map.

Info

NOTE: The key lines to focus on are line 26, where a value is added to the map, and line 29, where null is consistently returned.

Code Block
if (out.EU3LPG) {
    for (eu3CountryRecord in api.global.EU3CountryArray) {
        def highLowRMLogic = null
        def EU3countryName = eu3CountryRecord.attribute1

        def eu3countryRecord = api.local.countryMatrixMap.get(EU3countryName)

        def targetSIPriceLowRM = eu3countryRecord.get("TargetSIPriceLowRM")
        def targetSIPriceHighRM = eu3countryRecord.get("TargetSIPriceHighRM")
        def targetSIPriceHighMaxRM = eu3countryRecord.get("TargetSIPriceHighMaxRM")
        def targetSIPriceHighMinRM = eu3countryRecord.get("TargetSIPriceHighMinRM")

        if (targetSIPriceLowRM == null && targetSIPriceHighRM == null) {
            highLowRMLogic = null
        } else if (targetSIPriceLowRM == null) {
            highLowRMLogic = "Adjusted to High Comp Index Target"
        } else if (targetSIPriceHighMaxRM != null && targetSIPriceHighMinRM != null) {
            if (targetSIPriceHighMaxRM <= targetSIPriceLowRM && targetSIPriceLowRM <= targetSIPriceHighMinRM) {
                highLowRMLogic = "Adjusted to Low Comp Index Target"
            } else if (targetSIPriceLowRM > targetSIPriceHighMinRM) {
                highLowRMLogic = "Adjusted to High Min Comp Index Target"
            } else if (targetSIPriceLowRM < targetSIPriceHighMinRM) {
                highLowRMLogic = "Adjusted to High Max Comp Index Target"
            }
        }
        api.local.countryMatrixMap.get(EU3countryName).put("HighLowRMLogic", highLowRMLogic)
    }
}
return null

A now - test for such element can be coded like this:

Code Block
    def "HighLowRMLogic value stored in local map  - TargetSIPriceLowRM is null "() {
        when:

        def globalTable = ["EU3CountryArray": [["attribute1": "FRANCE", "attribute11": "FRANCE",], ["attribute1": "ROMANIA", "attribute11": "ROMANIA"]]]
        def localTable = ["countryMatrixMap": ["FRANCE" : ["TargetSIPriceLowRM": null, "TargetSIPriceHighRM": 10],
                                               "ROMANIA": [:]]]

        TestRun testRun = TestRun.builder()
                .withLogicTestDoubles(

Tip

CODE REVIEW: the above snippet of code is written in Groovy, it could be part of a larger function or process that is evaluating certain conditions to determine a logic called highLowRMLogic based on various target price attributes for countries within the EU3 region.

The code performs the following operations:

  1. It checks if the out.EU3LPG condition is true. If so, it proceeds to iterate over an array of country records using a for-loop.

  2. For each country record, it retrieves the country name and then fetches a corresponding record from a map called countryMatrixMap.

  3. It extracts four attributes from the country record: TargetSIPriceLowRM, TargetSIPriceHighRM, TargetSIPriceHighMaxRM, and TargetSIPriceHighMinRM.

  4. Several conditional checks are performed to determine the value of highLowRMLogic:

    • If both TargetSIPriceLowRM and TargetSIPriceHighRM are null, highLowRMLogic remains null.

    • If only TargetSIPriceLowRM is null, highLowRMLogic is set to "Adjusted to High Comp Index Target".

    • If both TargetSIPriceHighMaxRM and TargetSIPriceHighMinRM are not null, further conditions are checked to set highLowRMLogic to one of the three values: "Adjusted to Low Comp Index Target", "Adjusted to High Min Comp Index Target", or "Adjusted to High Max Comp Index Target" based on comparisons between TargetSIPriceLowRM, TargetSIPriceHighMaxRM, and TargetSIPriceHighMinRM.

  5. The resulting highLowRMLogic value is then put back into the countryMatrixMap for the respective country.

  6. Finally, the function returns null.

Test Scenario

The provided Groovy code defines a test scenario using a TestRun builder pattern to set up and execute a test for business logic. It involves the use of globalTable and localTable, which are maps containing configuration or data needed for the test.

Code Block
    def "HighLowRMLogic value stored in local map  - TargetSIPriceLowRM is null "() {
        when:

        def globalTable =   ["outEU3CountryArray": [["attribute1": "FRANCE", "attribute11": "FRANCE",],          ["attribute1": "ROMANIA", "attribute11": "ROMANIA"]]]
        def localTable = ["countryMatrixMap": ["FRANCE" : ["TargetSIPriceLowRM": null, "TargetSIPriceHighRM": 10],
                   "EU3LPG": true                         ],  "ROMANIA": [:]]]

        TestRun testRun = TestRun.builder()
        "api":        .withLogicTestDoubles(
                        ["globalout": globalTable,
[
                                "localEU3LPG": :true
localTable]                 )       ],
         .buildElementTest(LOGIC_DIR, ELEMENT_NAME)          and:    "api":
    Script script = testRun.getElementScript()          then:         testRun.execute().getElementTestResult() == null      ["global": globalTable,
                               localTable == ["countryMatrixMaplocal" : ["FRANCE": ["TargetSIPriceLowRM": null, "TargetSIPriceHighRM": 10, "HighLowRMLogic": "Adjusted to High Comp Index Target"], localTable]
                )
                .buildElementTest(LOGIC_DIR, ELEMENT_NAME)

        and:
        Script script = testRun.getElementScript()

        "ROMANIA":["HighLowRMLogic":null]]]

Most important lines are:

  • 5 - where input value of local table is defined.

  • 23 where we check if return value from elements is null

  • 24 - where we check if value in local map has been changed

Test Element or Function using function from another element

Let’s have element (simplified) using function from another element (line 3, element Library in the same logic).

Code Block
if ((out.PricingStrategyProposed || adjustmentFactorVs == "current") && out.IsResultElementNewListPrice) {then:
        testRun.execute().getElementTestResult() == null
       def newListPricelocalTable = (listPrice * (1 + adjustmentFactorPct)) + adjustmentFactorUnit== ["countryMatrixMap": ["FRANCE": ["TargetSIPriceLowRM": null, "TargetSIPriceHighRM": 10, "HighLowRMLogic": "Adjusted to High Comp Index Target"],
                             def newListPriceAfterThresholdsRounded = Library.applyThresholdOnResultPriceWithRounding(newListPrice, out.BaseListPrice, out.IncreaseThresholdPct, out.DecreaseThresholdPct)      return newListPriceAfterThresholdsRounded  }

This is possible if you have groovy in version 2.4.12 and lower, but not possible you you have higher version. See below:

→ If test is using a method called from another element it:

  • does not work with groovy higher than 2.4.12

  • does work with groovy 2.4.12.

It means, those tests works on gitlab server in pipeline but ususally does not work on consultant localhost. After server will be upgraded to higher version of Groovy, it will break (some) test on (at least) following project:

  • Michelin France

  • Schnider

  • MediaSaturn

It seem TDD4C needs to be update to work with newer version of groovy.

Let me quickly describe the case:
I have following element test

Code Block
def "New PME returns price with applied Threshold"() {
        when:
        /*
        def lpgLibScript = TestRun.builder()
                .buildElementTest(LOGIC_DIR, "LPGLibrary")
                .getElementScript()
         */

        TestRun testRun = TestRun.builder()
                .withLogicTestDoubles("out": [
                        "AdjustmentFactorVs"   : "current",
                        "CurrentPME"           : 10,
                        "BasePME"              : 11,
                        "IncreaseThresholdPct" : 12,
                        "DecreaseThresholdPct" : 13,
                        "ProposedPME"          : 14,
                        "IsResultElementNewPME": true
                ])
                .withAdditionalElementInitialized("Library")
                .buildElementTest(LOGIC_DIR, ELEMENT_NAME)
        then:
        testRun.execute()
                .getElementTestResult() == 10
    }

...

"ROMANIA":["HighLowRMLogic":null]]]

Tip

CODE REVIEW: Above is a nippet of Groovy code, it defines a test scenario using a TestRun builder pattern to set up and execute a test for some business logic.

The globalTable and localTable are defined as maps containing configuration or data needed for the test. The globalTable contains an array mapped to the key "EU3CountryArray" with two entries representing France and Romania. The localTable contains a map called "countryMatrixMap" with entries for France and Romania, where France has a "TargetSIPriceLowRM" key with a null value and a "TargetSIPriceHighRM" key with a value of 10.

Info

NOTE: The most important lines of code are:

  • Line # 5 - where input value of local table is defined.

  • Line #23 where we check if return value from elements equals null.

  • Line #24 - where we check if value in local map has been changed.

Here is the sequence of events:

  • A TestRun object is created with a builder pattern, setting up some logic test doubles, which seem to be mock data or configurations for the test.

  • The "out" key is set to a map containing "EU3LPG" as true, and the "api" key is set to a map with the "global" and "local" keys mapped to the previously defined tables.

  • The test is built with unspecified LOGIC_DIR and ELEMENT_NAME variables, possibly indicating the directory and name of the logic element being tested.

  • After building the test element, the test is executed, and it seems to expect that the result of the test execution should be null (getElementTestResult() == null).

  • Finally, there is an assertion that after the test execution, the localTable should have been modified such that the entry for France now includes a "HighLowRMLogic" key with the value "Adjusted to High Comp Index Target", while the entry for Romania should have a "HighLowRMLogic" key with a null value.

Info

NOTE: This code could very likely be part of an automated test suite used for validating business logic in a software application. The specific logic being tested here seems to involve price ranges and country-specific configurations.

Test Element or Function using Function from another Element

...

This section discusses an element that uses a function from another element, specifically involving Groovy version compatibility. It highlights that the functionality works with Groovy 2.4.12 but not with higher versions.

Also, it highlights the impact on testing in different environments, such as on a GitLab server versus a local machine and identifies potential projects that may be affected by this compatibility issue. This issue suggests that an update to TDD4C is needed to work with newer versions of Groovy. This information provides insights into the challenges related to version compatibility and testing in specific software development projects.

Let’s have element (simplified) using function from another element (line 3, element Library in the same logic).

Code Block
if ((out.PricingStrategyProposed || adjustmentFactorVs == "current") && out.IsResultElementNewListPrice) {
    def newListPrice = (listPrice * (1 + adjustmentFactorPct)) + adjustmentFactorUnit
    def newListPriceAfterThresholdsRounded = Library.applyThresholdOnResultPriceWithRounding(newListPrice, out.BaseListPrice, out.IncreaseThresholdPct, out.DecreaseThresholdPct)

    return newListPriceAfterThresholdsRounded 
}
Tip

CODE REVIEW: this is a snippet of Groovy code that outlines a conditional process that determines a new list price for an item based on certain criteria.

Here's a breakdown of the logic:

  1. The if statement checks two conditions: whether out.PricingStrategyProposed is true or adjustmentFactorVs equals "current", and whether out.IsResultElementNewListPrice is true.

  2. If both conditions are satisfied, the code inside the if block is executed.

  3. Inside the if block, a new variable newListPrice is defined. It is calculated by taking the listPrice, multiplying it by (1 + adjustmentFactorPct), and then adding adjustmentFactorUnit. This represents an adjustment to the original list price by a certain percentage and a fixed unit.

  4. Another variable, newListPriceAfterThresholdsRounded, is defined. It is calculated by calling a method named Library.applyThresholdOnResultPriceWithRounding.

  5. This method seems to apply some form of threshold limits to the new list price and includes rounding. The parameters passed to this method are the newly calculated newListPrice, out.BaseListPrice, out.IncreaseThresholdPct, and out.DecreaseThresholdPct.

  6. Finally, the code returns the value of newListPriceAfterThresholdsRounded.

Info

NOTE: This code could be part of a larger pricing strategy algorithm where prices are adjusted based on various factors and thresholds. The method applyThresholdOnResultPriceWithRounding likely ensures that the new price does not exceed certain upper and lower bounds.

Groovy Version Incompatibility

Let’s discuss the compatibility of a test method when using Groovy in different versions, using code example above, the method works with Groovy version 2.4.12, but does not work with higher versions. This implies that the method is compatible with Groovy 2.4.12 and lower, but not with higher versions.

The conclusion is that those tests work on gitlab server in pipeline but usually don’t work on Configuration Engineer’s localhost. Once the server is upgraded to a higher version of Groovy, it will cause some tests to fail in one or more projects.

Info

NOTE: The resolution would be update TDD4C to work with newer version of groovy.

Allow me to provide a quick description of the scenario, I have the following element test show below:

Code Block
def "New PME returns price with applied Threshold"() {
        when:
        /*
        def lpgLibScript = TestRun.builder()
                .buildElementTest(LOGIC_DIR, "LPGLibrary")
                .getElementScript()
         */

        TestRun testRun = TestRun.builder()
                .withLogicTestDoubles("out": [
                        "AdjustmentFactorVs"   : "current",
                        "CurrentPME"           : 10,
                        "BasePME"              : 11,
                        "IncreaseThresholdPct" : 12,
                        "DecreaseThresholdPct" : 13,
                        "ProposedPME"          : 14,
                        "IsResultElementNewPME": true
                ])
                .withAdditionalElementInitialized("Library")
                .buildElementTest(LOGIC_DIR, ELEMENT_NAME)
        then:
        testRun.execute()
                .getElementTestResult() == 10
    }
Tip

CODE REVIEW: this is a Groovy code snippet from a test case written in Groovy and the purpose of this test case is to verify that a "New PME" (Price Management Engine, possibly) returns the correct price after applying a certain threshold.

The test case is structured in the Given-When-Then format typical for behavior-driven development (BDD). The "when" block is commented out, which suggests that some initial setup code has been temporarily disabled or is not relevant to the current context.

Lets review the actual active code and highlight the events:

  • A TestRun object is being built with specific test doubles. These doubles are mock values that represent the inputs or conditions under which the system under test operates.

  • It will use keys such as "AdjustmentFactorVs", "CurrentPME", "BasePME", "IncreaseThresholdPct", "DecreaseThresholdPct", "ProposedPME", and "IsResultElementNewPME" as variables or parameters that the Price Management Engine uses to calculate the final price.

  • The withLogicTestDoubles method is used to provide these mock values, and withAdditionalElementInitialized("Library") suggests that an additional component or library is being initialized for this test.

  • Finally, the "then" block asserts that the result of executing the test (testRun.execute().getElementTestResult()) should equal 10.

  • This assertion checks whether the output of the Price Management Engine matches the expected value, given the test conditions.

Info

NOTE: this piece of code is meant to ensure that the logic for calculating prices meets the expected behavior when certain thresholds are applied. It's a typical unit test that aims to validate the correctness of a specific function or method within a larger codebase.

Illustrating Groovy Incompatibility

Now, let’s describe an issue with a test failing in Groovy versions higher than 2.4.12 due to a method not being found in the Library element, which seems to be improperly initialized. We will also assert that mocking the function via withLogicTestDoubles and using the function withAdditionalElementInitialized does not work properly.

Here is the incompatibility scenario:

  • Using an element with following code (simplified):

Code Block
return Library.applyThresholdOnResultPrice(proposedPME, out.BasePME, out.IncreaseThresholdPct, out.DecreaseThresholdPct)
  • As element calls function from another element, I have also element Library with following function (simplified):

Code Block
def applyThresholdOnResultPrice(def priceBeforeThresholds, def basePrice, def increaseThreshold, def decreaseThreshold) {
    return test 
}
  • In

...

  • Groovy higher than 2.4.12 the test will fail with following error:

Code Block
Condition failed with Exception:

testRun.execute() .getElementTestResult() == 10
|       |
|       groovy.lang.MissingMethodException: No signature of method: static Library.applyThresholdOnResultPrice() is applicable for argument types: (java.lang.Integer, java.lang.Integer,
java.lang.Integer, java.lang.Integer) values: [10, 11, 12, 13]
|
testRun.execute() .getElementTestResult() == 10
|       |
| Possible solutions: applyThresholdOnResultPrice(java.lang.Object, java.lang.Object, java.lang.Object, java.lang.Object)
net.pricefx.tdd4c.TestRun@d02f8d

Simply - it cannot find method from Library element as it seems Library element was not Initialized correctly. This is fine in groovy 2.4.12. ←

...

 groovy.lang.MissingMethodException: No signature of method: static Library.applyThresholdOnResultPrice() is applicable for argument types: (java.lang.Integer, java.lang.Integer, java.lang.Integer, java.lang.Integer) values: [10, 11, 12, 13]
|       Possible solutions: applyThresholdOnResultPrice(java.lang.Object, java.lang.Object, java.lang.Object, java.lang.Object)

...

This needs to be solved on TDD4C side, so do not loose your time here.

...


net.pricefx.tdd4c.TestRun@d02f8d
Info

NOTE: it cannot find method from Library element as it seems Library element was not Initialized correctly. However, this is fine in groovy 2.4.12.