...
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 | ||
---|---|---|
| ||
.. .. 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
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 |
The code performs the following operations:
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.For each country record, it retrieves the country name and then fetches a corresponding record from a map called
countryMatrixMap
.It extracts four attributes from the country record:
TargetSIPriceLowRM
,TargetSIPriceHighRM
,TargetSIPriceHighMaxRM
, andTargetSIPriceHighMinRM
.Several conditional checks are performed to determine the value of
highLowRMLogic
:If both
TargetSIPriceLowRM
andTargetSIPriceHighRM
are null,highLowRMLogic
remains null.If only
TargetSIPriceLowRM
is null,highLowRMLogic
is set to "Adjusted to High Comp Index Target".If both
TargetSIPriceHighMaxRM
andTargetSIPriceHighMinRM
are not null, further conditions are checked to sethighLowRMLogic
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 betweenTargetSIPriceLowRM
,TargetSIPriceHighMaxRM
, andTargetSIPriceHighMinRM
.
The resulting
highLowRMLogic
value is then put back into thecountryMatrixMap
for the respective country.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 The |
Info |
---|
NOTE: The most important lines of code are:
|
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
andELEMENT_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:
|
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 |
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, andwithAdditionalElementInitialized("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. |