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:
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
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.
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.
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.
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.
If the features are limited to display options, non-critical business data, or non-critical reports, lower unit test coverage may be deemed acceptable.
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 definesnewPME
andbasePME
using values from theout
object.If both
newPME
andbasePME
are not null, andbasePME
is not zero, it calculates the percentage change by dividingnewPME
bybasePME
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 definesnewListPrice
andbaseListPrice
from theout
object.Similarly to the first condition, if both
newListPrice
andbaseListPrice
are not null andbaseListPrice
is not zero, it calculates the percentage change by dividingnewListPrice
bybaseListPrice
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 totrue
.After building the test with
.buildElementTest(LOGIC_DIR, ELEMENT_NAME)
, it proceeds to the execution phase withtestRun.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 totrue
.
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. TheTestRun
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 byLOGIC_DIR
andELEMENT_NAME
.The
then:
block contains the actual test execution, where thetestRun.execute()
method is called, and its result is compared with anexpectedResult
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:
It filters the
retailerSIRecords
to include only those records whereRetailerSIPriceEUR
is not null.It declares a variable
retailerSIPrice
without initializing it.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 toretailerSIPrice
.If empty, it sets
retailerSIPrice
to null.
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 unspecifiedLOGIC_DIR
andELEMENT_NAME
.The
then
part of the test asserts that when thegetRetailerSIPrice
method of the element script is called withretailerSIRecords
, it should return11.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
andendMonthExternalPricingSI
).Line 8 sets up a
globalTable
map with key-value pairsLines 9-27 construct a
TestRun
object using a builder pattern. TheTestRun
is configured with logic test doubles for mocked API calls, such asfindLookupTable
,getMaxFindResultsLimit
, andfind
. Thefind
method seems to be stubbed to return a hardcoded list of maps with country keys and correspondingattribute10
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 correspondingRetailerSIPriceEUR
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 thegetMaxFindResultsLimit
andstream
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]] }
How to test function if it modifies local or global table and always returns null
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
How to test element / 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 .