Skip to end of metadata
Go to start of metadata

You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 9 Next »

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.

TDD4C stands for Test Driven Development 4 Consulting. It is a tiny project that allows logics (formulas) to be run locally in your favorite Java IDE or on any Continuous Integration instance (e.g. Jenkins, Team City, ect.). The main goal is to provide simple and easy way of writing unit tests for existing and newly created logics. 

LEARN MORE: To understand more about TDD4c, click here. This article is focused mainly on sample of unit tests when TDD4C is already deployed on project.

Why and When to Write Unit Tests?

The guidelines for unit testing in TDD4C are as follows:

  • Aim for a balance between comprehensive logic coverage and practicality, considering the size and longevity of the application, as well as developer turnover.

  • Adapt the level of unit testing to the frequency and scale of change management, with more extensive testing for applications undergoing frequent and significant changes.

  • Tailor the unit test coverage to the impact of broken features, ensuring higher coverage for critical functions that could result in financial losses or data exposure.

More standard approaches to unit testing:

  • On one end of the spectrum, there is the approach advocating for 100% logic coverage through unit tests, suggesting that every if statement and edge case should be tested.

  • On the opposite end, there is the perspective of avoiding unit tests altogether due to the time-consuming nature of refactoring and maintaining tests. This approach argues that the time and effort required for testing outweigh the benefits of developing new features and addressing bugs.

Ideally you should be as closest as possible to the first approach, but you have to consider:

  1. Size of the application, when considering the size of the application, it's important to assess whether it is a small, one-time use app or a larger, long-term application with multiple modules. For applications with longevity and potential developer turnover, it is advisable to have a higher number of unit tests

  2. Change management frequency, change management frequency also plays a role, with the level of unit testing being adjusted based on the frequency and scale of changes.

    1. If there are no plans to change the application or if only small changes are anticipated, a limited number of unit tests may be deemed sufficient.

    2. In the case of a few change requests every week and significant interventions to the current logic, it is essential to ensure that no functionality is inadvertently broken.

  3. Impact of broken feature, should inform the extent of unit test coverage, with critical functions requiring higher coverage to mitigate potential financial losses and data exposure.

    1. If the features are limited to display options, non-critical business data, or non-critical reports, lower unit test coverage may be deemed acceptable.

    2. When a bug can result in financial losses, negative margins, or the exposure of critical customer data, it is crucial to ensure thorough testing of the affected areas.

Samples of Unit Tests

We will explore these two types of unit testing:

  • Element Results

  • Function Testing

How To Test Element Result

In Cypress, testing an element result typically involves using Cypress commands to locate and interact with specific elements in the application under test. Here's a summary of how to test an element result in Cypress:

  • Locate the Element: Use Cypress commands such as cy.get(), cy.contains(), or cy.find() to locate the element you want to test. You can use CSS selectors, XPath, or other methods to target the element.

  • Interact with the Element: Once the element is located, you can interact with it using Cypress commands like click(), type(), or invoke() to simulate user actions such as clicking, typing, or invoking specific behaviors.

  • Assert the Result: After interacting with the element, use Cypress assertions such as should() to verify the expected result. For example, you can assert that the element contains specific text, has a certain class, or is visible on the page.

  • Handle Asynchronous Behavior: Cypress handles asynchronous behavior automatically, so you can chain commands and assertions to ensure that the test waits for the element result before proceeding.

  • View Test Results: When running the test in the Cypress Test Runner, you can view the test results in real-time, including any passed or failed assertions related to the element result.

By following these steps, you can effectively test element results in Cypress, ensuring that the application's UI behaves as expected and that specific elements produce the desired outcomes.

Overview of Element Result Test

Element result test are tests checking the result of the element itself. This is suitable for elements that returns any value in priceList, LPG or any other logic.

For this sample, let's have simple elements called “FinalPriceEvolPct”. It returns either evolution value in percent or null if conditions are not satisfied. Its result is dependent on result of another elements.

Element code is this:

if (out.IsResultElementNewPME) {
    def newPME = out.NewPME
    def basePME = out.BasePME

    if(newPME != null && basePME !=null && basePME !=0){
        return (newPME / basePME) - 1
    }
} else if(out.IsResultElementNewListPrice){
    def newListPrice = out.NewListPrice
    def baseListPrice = out.BaseListPrice

    if(newListPrice !=null && baseListPrice !=null && baseListPrice !=0){
        return (newListPrice / baseListPrice) - 1
    }
}

return null

NOTE: Here's a breakdown of what the code is doing:

  • The code first checks if the result element IsResultElementNewPME is true. If so, it defines newPME and basePME using values from the out object.

  • If both newPME and basePME are not null, and basePME is not zero, it calculates the percentage change by dividing newPME by basePME and subtracting 1. The result of this calculation is then returned.

  • If the first condition is not met, the code checks another condition: if IsResultElementNewListPrice is true. If this is the case, it defines newListPrice and baseListPrice from the out object.

  • Similarly to the first condition, if both newListPrice and baseListPrice are not null and baseListPrice is not zero, it calculates the percentage change by dividing newListPrice by baseListPrice and subtracting 1. This result is then returned.

  • If neither condition is met, the code returns null.

CODE REVIEW: The percentage change calculation formula used here is a common method to determine the relative change between a new and a base value. The formula used is:

Percentage Change=(New ValueBase Value)−1Percentage Change=(Base ValueNew Value​)−1

This code is likely part of a larger program that deals with financial or pricing data, as indicated by the variables PME (which could stand for something like 'Price Market Estimate' or similar) and ListPrice. The script ensures that division by zero does not occur and handles null values to prevent runtime errors.

Running Element Result Test

Next, we will test for this element to check what happens if all other elements are null. Expected result is null. Other elements referred in code like “out.NewPME” are defined to be null in function “.withLogicTestDoubles"

    def "FinalPriceEvolPct returns null if all inputs are null for NewPME result element"() {
        when:
        TestRun testRun = TestRun.builder()
                .withLogicTestDoubles("api" : [],
                        out: [
                                "NewPME": null,
                                "BasePME":null,
                                "IsResultElementNewPME":true
                        ])
                .buildElementTest(LOGIC_DIR, ELEMENT_NAME)

        then:
        testRun.execute()
                .getElementTestResult() == null
    }

This snippet of code written in Groovy, defines a test case for a method or function named FinalPriceEvolPct. The purpose of the test is to verify that the FinalPriceEvolPct function returns null when all its inputs are null. The inputs being checked are "NewPME", "BasePME", and a boolean flag "IsResultElementNewPME".

Here is the breakdown of the creating the testing scenario:

  • The test is structured using a fluent API style, common in testing frameworks like Spock, which Groovy often uses.

  • The test case is built using a TestRun.builder() method chain, which sets up the test doubles for an "api"

  • The output expectations as null for "NewPME" and "BasePME", with "IsResultElementNewPME" set to true.

  • After building the test with .buildElementTest(LOGIC_DIR, ELEMENT_NAME), it proceeds to the execution phase with testRun.execute()

  • Then it asserts that the result of the function under test should be null with .getElementTestResult() == null.

This test ensures that the function behaves correctly in scenarios where it should not perform any calculations or return any values due to the absence of input data.

Element Result Modifications

Next, we can do some additional modifications by modifying the input values NewPme, BasePME and IsResultElementNewPME and check the output value. If anybody will do a change in this element in future, we will identify it very quickly as test will fail.

This sample shows if NewPME is equal to 100, BasePME is equal to 20 and IsResultElementNewPME is equal to true, result must be 4.

    def "FinalPriceEvolPct returns calculated value for NewPME result element"() {
        when:
        TestRun testRun = TestRun.builder()
                .withLogicTestDoubles("api" : [],
                        out: [
                                "NewPME": 100,
                                "BasePME":20,
                                "IsResultElementNewPME":true
                        ])
                .buildElementTest(LOGIC_DIR, ELEMENT_NAME)

        then:
        testRun.execute()
                .getElementTestResult() == 4
    }

NOTE: This shows a test case written in a programming language that resembles Groovy, which is often used for scripting in Java environments, particularly for writing tests in frameworks like Spock.

This specific test case is named "FinalPriceEvolPct returns calculated value for NewPME result element". It seems to define a scenario where a TestRun object is being built with certain parameters. The parameters include a withLogicTestDoubles method call that seems to be mocking an API call with an empty array, and an out map that contains three key-value pairs:

  • "NewPME" set to 100,

  • "Base PME" set to 20,

  • "IsResultElementNewPME" set to true.

CODE REVIEW: The test case then proceeds to build and execute the test, checking if the result of the getElementTestResult() method call equals 4. The purpose of this test is likely to verify that a specific piece of business logic, which calculates a final price evolution percentage, is functioning as expected when given the input parameters specified.

This example is part of a behavior-driven development (BDD) framework, where the test cases are written in a human-readable format. The "when" block describes the setup or conditions under which the test is executed, and the "then" block describes the expected outcome.

The code snippet is intended to be self-contained and descriptive of its purpose, which allows for easier understanding and maintenance of the test case. It's a good example of how modern testing frameworks allow for readable and maintainable test definitions.

Creating Additional Element Result Tests

It's advisable to write the remaining tests for the other combinations of input parameters:

  • what happens if NewPME is 100and BasePME 20?

  • what happens if BasePME is null and NewPME 100?

  • what happens if BasePME is -1000 and NewPME -200?

  • what happens if IsResultElementNewPME is null and NewPME is 100and BasePME 20?

  • and so on…

In a scenario where you only modify the test input and test result while keeping the rest of the test the same, you have two options: you can either write a standalone test for each combination of input and result, or you can write a single test with a "where" clause that defines the input-output combinations.

Let’s use the following sample:

    def "FinalPriceEvolPct returns calculated value for NewPME result element"() {
        when:
        TestRun testRun = TestRun.builder()
                .withLogicTestDoubles("api": [],
                        out: [
                                "NewPME"               : NewPme,
                                "BasePME"              : BasePME,
                                "IsResultElementNewPME": IsResultElementNewPME
                        ])
                .buildElementTest(LOGIC_DIR, ELEMENT_NAME)

        then:
        testRun.execute()
                .getElementTestResult() == expectedResult

        where:
        NewPme | BasePME | IsResultElementNewPME | expectedResult
        100    | 20      | true                  | 4
        null   | 100     | true                  | null
        -1000  | -200    | true                  | 4
    }

This snippet of code written in Groovy, defines a function named "FinalPriceEvolPct" that is intended to return a calculated value for a result element labeled "NewPME."

From this code snippet we can deduce the following:

  • The function begins with the declaration and then sets up a TestRun object using a builder pattern. The TestRun object is configured with an empty array for "api" and a map for the "out" parameter, which includes "NewPME," "BasePME," and "IsResultElementNewPME" as keys with corresponding variables that are expected to be provided when the test is executed.

  • The .buildElementTest(LOGIC_DIR, ELEMENT_NAME) method call suggests that this function is part of a larger testing framework where specific elements are tested against a set of logic rules defined in directories or files referenced by LOGIC_DIR and ELEMENT_NAME.

  • The then: block contains the actual test execution, where the testRun.execute() method is called, and its result is compared with an expectedResult variable using the Groovy assert statement (==).

  • The where: block defines the data that will be used for the test cases. It's structured as a table with columns for "NewPme," "BasePME," "IsResultElementNewPME," and "expectedResult." Each row represents a different test case with values provided for each column:

    • A case where "NewPme" is 100, "BasePME" is 20, and "IsResultElementNewPME" is true, expecting the result to be 4.

    • A case where "NewPme" is null, "BasePME" is 100, and "IsResultElementNewPME" is true, expecting the result to be null.

    • A case where "NewPme" is -1000, "BasePME" is -200, and "IsResultElementNewPME" is true, expecting the result to be 4.

NOTE: This test setup allows for data-driven testing with given-when-then syntax. The code snippet is part of a test script designed to verify that the calculation of the percentage evolution of the final price based on new and base PME values behaves as expected under different scenarios.

How to Test a Function

In Cypress, testing a function involves writing test cases within a Cypress test file, using Cypress commands and assertions to exercise the function with different inputs and scenarios, and asserting the expected outcomes or behavior.

The main difference compared to an "element test" is that an element test uses the function .getElementTestResult(), while a function test uses the function .getElementScript().getRetailerSIPrice to run a method from the element itself.

To test a function in Cypress, you can follow these general steps:

  • Set Up Test File: Create a Cypress test file (typically a .spec.js file) within the "integration" folder of your Cypress project. This file will contain the test cases for your function.

  • Import Function: If the function you want to test is defined in a separate JavaScript file, import it into your test file using ES6 import syntax or any other suitable method.

  • Write Test Cases: Within the test file, use Cypress commands and assertions to write test cases that exercise the function under different scenarios. This may involve calling the function with different input values and asserting the expected output or behavior.

  • Arrange-Act-Assert: Structure your test cases using the Arrange-Act-Assert pattern. Arrange the test environment and inputs, act by calling the function, and assert the expected outcomes or side effects.

  • Use Cypress Commands: Utilize Cypress commands such as cy.wrap(), cy.invoke(), or cy.then() to interact with the function and handle asynchronous behavior if necessary.

  • Handle Mocking and Stubs: If the function interacts with external services or APIs, consider using Cypress plugins like cy.intercept() to mock API responses or stub external dependencies for isolated testing.

  • Run Tests: Use the Cypress Test Runner to run your test file and observe the results. You can view the test execution in real-time and inspect any passed or failed assertions related to the function being tested.

Testing a Function

The function definition inside an element can be tested separately to observe how the function behaves with different inputs.

For example, using the following element called RetailerSIPrice (code is not much important for you so you do not need to study it in details):

..
.. 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
}

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.

Perform Function Test

We will now create a basic test for this function. The test will verify the function's result when the input list is null using the code below:

    def "getRetailerSIPrice returns null if retailerSIRecords is null"() {
        when:

        def retailerSIRecords = null

        TestRun testRun = TestRun.builder()
                .buildElementTest(LOGIC_DIR, ELEMENT_NAME)
        then:
        testRun.getElementScript().getRetailerSIPrice(retailerSIRecords) == null

    }

CODE REVIEW: Illustrates a test written in the Groovy that performs the following:

  • checks whether the "getRetailerSIPrice" function returns null when the "retailerSIRecords" input is null.

  • The "when" block sets the "retailerSIRecords" variable to null,

  • The "then" block verifies that calling the "getRetailerSIPrice" function with the null input returns null.

This test is part of the process of testing the behavior of the "getRetailerSIPrice" function when given different inputs.

NOTE: If the input list is null, the result of the function is also null. This test is designed to uncover any unhandled null checks.

Now, let's use a non-null input for the second test using the following code:

    def "getRetailerSIPrice calculates only records with RetailerSIPriceEUR value"() {
        when:

        def retailerSIRecords = [["Country": "FRANCE", "RetailerSIPriceEUR": 10],
                                 ["Country": "FRANCE", "RetailerSIPriceEUR": 11],
                                 ["Country": "FRANCE", "RetailerSIPriceEUR": 12],
                                 ["Country": "FRANCE", "RetailerSIPriceEUR": 13],
                                 ["Country": "FRANCE", "MyPrice": 14]]

        TestRun testRun = TestRun.builder()
                .buildElementTest(LOGIC_DIR, ELEMENT_NAME)
        then:
        testRun.getElementScript().getRetailerSIPrice(retailerSIRecords) == 11.5
    }

CODE REVIEW: this code snippet is a test case written in Groovy, it is named "getRetailerSIPrice calculates only records with RetailerSIPriceEUR value".

The test is setting up a list of maps called retailerSIRecords, each map representing a record with a country and a price in EUR. Notably, the last record has a different key, "MyPrice", instead of "RetailerSIPriceEUR".

The execution of this test case results in:

  • Creates an instance of TestRun using a builder pattern, and seems to be setting up some kind of element test with unspecified LOGIC_DIR and ELEMENT_NAME.

  • The then part of the test asserts that when the getRetailerSIPrice method of the element script is called with retailerSIRecords, it should return 11.5.

  • This likely means that the method should calculate the average of the "RetailerSIPriceEUR" values, ignoring any records that do not have this key (as is the case with the last record).

Given that there are four valid "RetailerSIPriceEUR" records with values 10, 11, 12, and 13, their average indeed is 10+11+12+134=11.5410+11+12+13​=11.5, which is what the test expects.

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

Complex Test Definitions

The test definitions cover various 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 following sample test definition:

  • 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

Mock Pricefx API functions

Mocking Pricefx API functions covers various aspects such as mocking API functions, testing function behavior with different inputs, and mocking global or local variables. It also includes examples of mocking "api.stream" and another element's result. Additionally, it addresses challenges related to function testing and provides insights into complex test definitions.

Mock findLookupTable or find (or any other)

Mocking the findLookupTable function includes examples of how to mock this function using the withLogicTestDouble function or as part of withLogicTestDoubles. It also provides insights into testing functions that modify local or global tables and always return null, as well as mocking other elements' results. Additionally, it addresses the mocking of API functions, such as findLookupTable or find, and how to mock "api.stream" and api.global or api.local variables.

For example, we can do it via withLogicTestDouble function or as part of withLogicTestDoubles function. The function withLogicTestDoubles simply contains “api” map, with mocked api functions.

    def "getRetailerSIRecords returns records as array of maps with values in big decimal"() {
        when:

        def countryArray = ["FRANCE", "ROMANIA"]
        def extTyreId = "515145"
        def startMonthExternalPricingSI = "201901"
        def endMonthExternalPricingSI = "202401"
        def globalTable = ["EU3CountryArray": [["attribute1": "FRANCE", "attribute11": "FRANCE",], ["attribute1": "ROMANIA", "attribute11": "ROMANIA"]]]

        TestRun testRun = TestRun.builder()
                .withLogicTestDoubles(["api": [
                        "global"                : globalTable,
                        "findLookupTable"       : { String name -> return ["id": "10"] },
                        "getMaxFindResultsLimit": { -> return 200 },
                        "find"                  : { String typecode, Integer start, Integer end, String sortBy, def fieldsList, def filter1, def filter2, def filter3, def filter4, def filter5 ->
                            return [
                                    ["key1": "FRANCE", "attribute10": "64.6"],
                                    ["key1": "FRANCE", "attribute10": "64.6"],
                                    ["key1": "FRANCE", "attribute10": "64.6"],
                                    ["key1": "FRANCE", "attribute10": "64.6"],
                                    ["key1": "FRANCE", "attribute10": "64.6"],
                                    ["key1": "ROMANIA", "attribute10": "63.4620732"],
                                    ["key1": "ROMANIA", "attribute10": "63.4620732"]
                            ]
                        }

                ], "libs"                   : getSharedLibrary()])
                .buildElementTest(LOGIC_DIR, ELEMENT_NAME)
        then:
        testRun.getElementScript().getRetailerSIRecords(countryArray, extTyreId, startMonthExternalPricingSI, endMonthExternalPricingSI) == [
                [Country: "FRANCE", RetailerSIPriceEUR: 64.6],
                [Country: "FRANCE", RetailerSIPriceEUR: 64.6],
                [Country: "FRANCE", RetailerSIPriceEUR: 64.6],
                [Country: "FRANCE", RetailerSIPriceEUR: 64.6],
                [Country: "FRANCE", RetailerSIPriceEUR: 64.6],
                [Country: "ROMANIA", RetailerSIPriceEUR: 63.4620732],
                [Country: "ROMANIA", RetailerSIPriceEUR: 63.4620732]]

    }

CODE REVIEW: snippet of code from a test script written in Groovy, it defines a test case for a method named getRetailerSIRecords that is expected to return an array of maps with values in BigDecimal format.

Here's a breakdown of the key parts of the script:

  • Lines 4-7 define variables for the parameters that will be passed to the getRetailerSIRecords method. These include arrays of countries (countryArray), an external tire ID (extTyreId), and date ranges for external pricing (startMonthExternalPricingSI and endMonthExternalPricingSI).

  • Line 8 sets up a globalTable map with key-value pairs

  • Lines 9-27 construct a TestRun object using a builder pattern. The TestRun is configured with logic test doubles for mocked API calls, such as findLookupTable, getMaxFindResultsLimit, and find. The find method seems to be stubbed to return a hardcoded list of maps with country keys and corresponding attribute10 values representing prices.

  • Lines 28-29 indicate that the test is built for a specific logic directory (LOGIC_DIR) and element name (ELEMENT_NAME), which are placeholders for actual paths or identifiers.

  • Lines 30-38 show the then: section where the actual test execution takes place.

  • The getRetailerSIRecords method is called with the previously defined parameters, and the expected output is an array of maps with countries and their corresponding RetailerSIPriceEUR values in BigDecimal format. The prices for "FRANCE" are consistently "64.6", while prices for "ROMANIA" are "63.4620732".

This test case appears to validate that the method correctly retrieves and formats pricing information for different countries within specified date ranges. The use of BigDecimal is important for financial calculations to avoid precision loss associated with floating-point arithmetic.

Mock of api.stream

Mocking the api.stream function includes defining api.stream similarly to other API functions and adding ".stream()" at the end of the values list. It also provides examples of testing functions that modify local or global tables and always return null, as well as mocking another element's result.

Additionally, it addresses more complex test definitions, such as mocking API functions like findLookupTable or find, function testing using a function from another element, and mocking global or local variables.

For example, in the following sample we will define api.stream the same way as other api function, and add “.stream()” at the end of values list.

    def "getPXasMap return map with specified attribute as key"() {
        when:
        TestRun testRun = TestRun.builder()
                .withLogicTestDouble("api", [
                        "getMaxFindResultsLimit": { -> return 200 },
                        "stream"                : { String typeCode, def orderBy, def fields, def filters ->
                            return [
                                    [
                                            "attribute18": "*",
                                            "attribute17": "*",
                                            "attribute39": "*",
                                            "attribute37": "*"
                                    ],
                                    [
                                            "attribute18": "SNOW",
                                            "attribute17": "RIKEN",
                                            "attribute39": "PC",
                                            "attribute37": "Winter"
                                    ],
                                    [
                                            "attribute18": "ENERGY SAVER",
                                            "attribute17": "MICHELIN",
                                            "attribute39": "PC",
                                            "attribute37": "Summer"
                                    ]
                            ].stream()
                        }
                ])
                .buildElementTest LOGIC_DIR, ELEMENT_NAME

        and:
        Script script = testRun.getElementScript()

        then:
        script.getPXasMap("CommercialCatalog", "attribute18", ["attribute18", "attribute17", "attribute37", "attribute39"]) == ["*"           : ["attribute18": "*", "attribute17": "*", "attribute39": "*", "attribute37": "*"],
                                                                                                                                "SNOW"        : ["attribute18": "SNOW", "attribute17": "RIKEN", "attribute39": "PC", "attribute37": "Winter"],
                                                                                                                                "ENERGY SAVER": ["attribute18": "ENERGY SAVER", "attribute17": "MICHELIN", "attribute39": "PC", "attribute37": "Summer"]]
    }

CODE REVIEW: a code snippet from a test script written in Groovy, which is a scripting language that runs on the Java Virtual Machine (JVM). The script defines a method called getPXasMap which seems to be designed to return a map with a specified attribute as a key.

In this code we can discern the following:

  • The method is being tested using a TestRun object that is built with mock responses for an API. The mock API is set up to return specific values when the getMaxFindResultsLimit and stream methods are called.

  • The stream method in particular is mocked to return a list of maps, each containing attributes with keys like "attribute18", "attribute17", "attribute39", and "attribute37", and corresponding values such as "SNOW", "RIKEN", "PC", and "Winter" for one entry, and "ENERGY SAVER", "MICHELIN", "PC", and "Summer" for another.

  • The buildElementTest method seems to be used to prepare the test environment, specifying a logic directory and an element name.

NOTE: In the assertion part of the script (then block), it is expected that when the getPXasMap method is called with the parameters "CommercialCatalog", "attribute18", and a list of attributes, it will return a map where the keys are the values of "attribute18" and the values are the corresponding maps of attributes.

The expected outcome of the method is:

  • A map with keys like "*", "SNOW", and "ENERGY SAVER", each pointing to their respective maps containing various attributes and their values.

This is a typical pattern for writing unit tests in software development, where you set up conditions, invoke the method under test, and assert that the output matches your expectations.

Mock another Element Result

This is very common case - element or functions is using another element result and you have to mock it to your rest. You can simply do it with “out” map like this. See the line 5 and below.

    def "NewListPricePack Element test - all data defined returns calculated value"() {
        when:

        TestRun testRun = TestRun.builder()
                .withLogicTestDoubles("out": [
                        "CurrentListPrice"             : 10,
                        "NewSOPricePack"               : 20,
                        "SOPriceActual"                : 2,
                        "IsResultElementNewListPrice":true
                ])
                .buildElementTest(LOGIC_DIR, ELEMENT_NAME)

        and:
        Script script = testRun.getElementScript()

        then:
        testRun.execute()
                .getElementTestResult() == 100
    }

How to mock api.global or api.local variables

This is necessary if element or function you test is using any value from api.global or api.local table. See this example, especially lines 8 and 12. This sample defines global table, api.local table is defined the same way - just change the keyword to “local”.

   def "getRetailerSIRecords returns records as array of maps with values in big decimal"() {
        when:

        def countryArray = ["FRANCE", "ROMANIA"]
        def extTyreId = "515145"
        def startMonthExternalPricingSI = "201901"
        def endMonthExternalPricingSI = "202401"
        def globalTable = ["EU3CountryArray": [["attribute1": "FRANCE", "attribute11": "FRANCE",], ["attribute1": "ROMANIA", "attribute11": "ROMANIA"]]]

        TestRun testRun = TestRun.builder()
                .withLogicTestDoubles(["api": [
                        "global"                : globalTable,
                        "findLookupTable"       : { String name -> return ["id": "10"] },
                        "getMaxFindResultsLimit": { -> return 200 },
                        "find"                  : { String typecode, Integer start, Integer end, String sortBy, def fieldsList, def filter1, def filter2, def filter3, def filter4, def filter5 ->
                            return [
                                    ["key1": "FRANCE", "attribute10": "64.6"],
                                    ["key1": "FRANCE", "attribute10": "64.6"],
                                    ["key1": "FRANCE", "attribute10": "64.6"],
                                    ["key1": "FRANCE", "attribute10": "64.6"],
                                    ["key1": "FRANCE", "attribute10": "64.6"],
                                    ["key1": "ROMANIA", "attribute10": "63.4620732"],
                                    ["key1": "ROMANIA", "attribute10": "63.4620732"]
                            ]
                        }

                ], "libs"                   : getSharedLibrary()])
                .buildElementTest(LOGIC_DIR, ELEMENT_NAME)
        then:
        testRun.getElementScript().getRetailerSIRecords(countryArray, extTyreId, startMonthExternalPricingSI, endMonthExternalPricingSI) == [
                [Country: "FRANCE", RetailerSIPriceEUR: 64.6],
                [Country: "FRANCE", RetailerSIPriceEUR: 64.6],
                [Country: "FRANCE", RetailerSIPriceEUR: 64.6],
                [Country: "FRANCE", RetailerSIPriceEUR: 64.6],
                [Country: "FRANCE", RetailerSIPriceEUR: 64.6],
                [Country: "ROMANIA", RetailerSIPriceEUR: 63.4620732],
                [Country: "ROMANIA", RetailerSIPriceEUR: 63.4620732]]

    }

How to mock function from (shared) Library

Let’s say our function is using another function from shared library and we have to mock it. You can either mock it inside test or mock it as separate function. Here you can se how to do it. See the line 1-10 and 29. Keyword you need is “libs”

    def getSharedLibrary(String val) {
        return [
                "Library": [
                        "Conversions": [
                                "convertToBigDecimal": { value -> return value?.toBigDecimal() }
                        ]
                ]
        ]
    }
    
        def "getRetailerSIRecords returns records as array of maps with values in big decimal"() {
        when:

        def countryArray = ["FRANCE", "ROMANIA"]
        def extTyreId = "515145"
        def startMonthExternalPricingSI = "201901"
        def endMonthExternalPricingSI = "202401"
        def globalTable = ["EU3CountryArray": [["attribute1": "FRANCE", "attribute11": "FRANCE",], ["attribute1": "ROMANIA", "attribute11": "ROMANIA"]]]

        TestRun testRun = TestRun.builder()
                .withLogicTestDoubles(["api": [
                        "global"                : globalTable,
                        "findLookupTable"       : { String name -> return ["id": "10"] },
                        "getMaxFindResultsLimit": { -> return 200 },
                        "find"                  : { String typecode, Integer start, Integer end, String sortBy, def fieldsList, def filter1, def filter2, def filter3, def filter4, def filter5 ->
                            return [["key1": "FRANCE", "attribute10": "64.6"]]
                        }

                ], "libs"                   : getSharedLibrary()])
                .buildElementTest(LOGIC_DIR, ELEMENT_NAME)
        then:
        testRun.getElementScript().getRetailerSIRecords(countryArray, extTyreId, startMonthExternalPricingSI, endMonthExternalPricingSI) == [
                [Country: "FRANCE", RetailerSIPriceEUR: 64.6]]

    }
    

Test Function with Local or Global table

Let’s have a element that modifies local / global table 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 returned.

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:

    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(
                        "out": [
                                "EU3LPG": true
                        ],
                        "api":
                                ["global": globalTable,
                                 "local" : localTable]
                )
                .buildElementTest(LOGIC_DIR, ELEMENT_NAME)

        and:
        Script script = testRun.getElementScript()

        then:
        testRun.execute().getElementTestResult() == null
        localTable == ["countryMatrixMap": ["FRANCE": ["TargetSIPriceLowRM": null, "TargetSIPriceHighRM": 10, "HighLowRMLogic": "Adjusted to High Comp Index Target"],
                                            "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).

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 
}

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

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
    }

For element with following code (simplified):

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):

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:

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]
|       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. ←

You cannot mock the function via withLogicTestDoubles and function withAdditionalElementInitialized does not work properly. Please see the Jira issue for test sample and result (you will get following exception 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] )

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

And that’s it for this article, happy testing (smile) .

  • No labels