A quick way to get the duration of a test using Groovy

From a previous test I worked on, I wanted to point out how to record the execution time of a test. It uses the TimeCategory and TimeDuration libraries. Since my API test doesn't run within a Test Suite, it doesn't record the execution time, but it's still possible to get that information using code similar to the following.

import groovy.time.TimeCategory 
import groovy.time.TimeDuration

def timeStart = new Date()

//Test Code goes between the Start and Stop definitions

def timeStop = new Date()
TimeDuration duration = TimeCategory.minus(timeStop, timeStart)

log.logWarning("Execution Time: " + duration)

Other articles of interest:

Data Driven API Testing with Katalon using Spreadsheet Data

In expanding my use of Katalon Studio, I have started working on automated API testing. Like Postman, you can set up a manual API test to confirm a request/response combination. But it's possible to take that further and not only send multiple API requests, but validate the response.

For a really good tutorial on putting together the API test, take a look at this video. It steps through creating the API object and configuring parameters.

Katalon Studio API Testing

Following up from that, the code listed below creates a data driven API test wherein it reads data from an XLSX spreadsheeting using the POI model.

The purpose of the test is to send a proper request for an inventory item and confirm the price of the item is correct. The spreadsheet contains the inventory data and the price of the item.

Additionally, the test records the Response Time and the Status Code of the request. Within Postman this would display as 398 ms 200 OK.

The request is built using data from the spreadsheet and is sent to the API object using the WS.sendRequest command.

Determining the number of rows in the spreadsheet is handled by:
int rowCount = sheet.getLastRowNum()

Reading data from the spreadsheet is done by reading a cell value and assigning it to a variable:
int customer_number=sheet.getRow(loop).getCell(0).getNumericCellValue()
String uom=sheet.getRow(loop).getCell(3).getStringCellValue();

The response time is recorded as:

The status of the response is:

The response is parsed using the JsonSlurper
def api_response = slurper.parseText(api_request.getResponseBodyContent())

The different response items are found using the following method:
String itemPrice = api_response.pricing.tiers[0].price

Parsing the JSON correctly took a couple of tries as I was using pricing.tiers.price to traverse the hierarchy rather than seeing it as an array and using tiers[0].price. Using a site like got things sorted out.

For output, the test records several pieces of information:
The total number of requests
The number of pricing errors along with status code errors
A finally tally of the success rate

Using the code below, 100 requests can be processed in less than 20 seconds
1500 items can be processed in 5 minutes

This test will continue to expand to validate more data points, but at the moment, it's a solid gauge of whether the API is working, the response time, and accuracy.

import groovy.json.JsonSlurper as JsonSlurper

import org.apache.poi.xssf.usermodel.XSSFCell
import org.apache.poi.xssf.usermodel.XSSFRow
import org.apache.poi.xssf.usermodel.XSSFSheet
import org.apache.poi.xssf.usermodel.XSSFWorkbook


import com.kms.katalon.core.logging.KeywordLogger as KeywordLogger

import groovy.time.TimeCategory 
import groovy.time.TimeDuration

KeywordLogger log = new KeywordLogger()

def timeStart = new Date()
int responseTimeError, pricingError, statusCodeError, priceAvailableError, messageFailureError=0
int api_response_time
float successRate

def slurper = new JsonSlurper()

FileInputStream file = new FileInputStream ("//Postman Testing//Data Files//pricing.xlsx")
XSSFWorkbook workbook = new XSSFWorkbook(file);
//XSSFSheet sheet = workbook.getSheetAt(1);
XSSFSheet sheet = workbook.getSheet("pricing-100-items");
int rowCount = sheet.getLastRowNum()

'Read data from excel'
for (loop = 1; loop <=rowCount; loop++) {
    //Assign spreadsheet columns to variables
    int customer_number=sheet.getRow(loop).getCell(0).getNumericCellValue()
    int branch_number=sheet.getRow(loop).getCell(1).getNumericCellValue()
    String tier_code=sheet.getRow(loop).getCell(2).getStringCellValue();
    String uom=sheet.getRow(loop).getCell(3).getStringCellValue();
    String item_price=sheet.getRow(loop).getCell(4).getNumericCellValue();
    //Send API Request
    api_request = WS.sendRequest(findTestObject('Pricing Request', 
        [('customer_number') : customer_number, 
        ('branch_number') : branch_number, 
        ('tier_code') : tier_code, 
        ('uom') : uom]))
    //Store Response time
    //If the Status of the request is 200 (OK) process the response
    if (api_request.getStatusCode()==200){
        def api_response = slurper.parseText(api_request.getResponseBodyContent())
        //Store the item price
        String itemPrice = api_response.pricing.tiers[0].price
        //Verify there is no failure message
        String failure_message=api_response.pricing.tiers[0].failure_messages
        if (failure_message!='[]'){
            log.logError("ERROR: Failure Message on pricing request " + loop + " " + failure_message)
        //Verify that Price Available is true within the response
        String price_available = api_response.pricing.tiers[0].price_available
        if (price_available!="true"){
            log.logError("ERROR: Price Available error on pricing request " + loop + " " + price_available)
        //Display pricing details
        log.logWarning('Tier Code: ' + tier_code + ' <---> ' + 'Item Price: ' + itemPrice + ' <---> ' + 'Expected Price: ' + item_price + ' <---> ' + 'Response time: ' + api_response_time)
        if (itemPrice!=item_price){
            log.logError("ERROR: The returned price for request " + loop + " does not match the expected price")
    } else {
        log.logError("ERROR: There was an error with the pricing request " + loop + ". Error code: " + api_request.getStatusCode())
    if (api_response_time>10000){
        log.logError("The server response time is higher than expected with a time of: " + api_response_time)

def timeStop = new Date()
TimeDuration duration = TimeCategory.minus(timeStop, timeStart)

log.logWarning("<--- API Pricing Request Results --->")
log.logWarning(rowCount + " items priced:")
log.logWarning("There were " + responseTimeError + " requests with a higher than average response time")
log.logWarning("There were " + pricingError + " requests with a different price than expected")
log.logWarning("There were " + statusCodeError + " requests with an unexpected status code")
log.logWarning("There were " + priceAvailableError + " requests with a Price Available error")
log.logWarning("There were " + messageFailureError + " requests with a Message Failure error")
successRate = ((rowCount - (Integer.valueOf(pricingError) + Integer.valueOf(statusCodeError))) / rowCount) * 100
log.logWarning("Success rate: " + successRate.round(2) +"%" )
log.logWarning("Execution Time: " + duration)


Other articles of interest:

Determine if a checkbox has been checked/selected

On the surface it seems like an easy test, check if a checkbox has been selected and perform an action. After trying it out, the process doesn't follow the usual pattern.

My first attempt looked like this:

    True action
    False action

That works, but throws an exception when the condition is false. And that looks really bad in the log files.

The correct way to handle the situation is to add the FailureHandling option so you don't get the error message.

In the code below, the status of the checkbox is determined. If it's already checked, enter the search criteria into the input field. If the checkbox isn't selected, enter the search criteria AND check the checkbox.

For my situation, I need that box to be checked. It would be just as easy to add the code that unchecks it or does something else. But the main action happens within the IF statement and the FailureHandling.OPTIONAL. This allows the processing to continue and code flows normally.

if (WebUI.verifyElementChecked(findTestObject('Object Location/checkbox-My Branch Only'),10,FailureHandling.OPTIONAL)){
    WebUI.setText(findTestObject('Object Location/input-Inventory Search Field'), inventorySearch)

} else {
    WebUI.setText(findTestObject('Object Location/input-Inventory Search Field'), inventorySearch)'Object Location/checkbox-My Branch Only'))

Other articles of interest:

Changing the scope of a variable to be available within a Method

After reading the value from a dropdown, I wanted to use that piece of information for a comparison test in another part of the Test Case. That's not a problem, except that I want to use the variable within a defined Method within the test case. I've done this sort of thing before and passed the needed values to the method. The Method didn't need any parameters, so I looked for another way to make that variable more "global" in scope.

Turns out this can be done using @Field

The first step is to import the correct library

import groovy.transform.Field

From there, "flag" the corresponding variable and change it's scope.

@Field String quoteBranchName
log.logWarning('The listed Branch in the quote is: ' + quoteBranchName)

That variable can now be referenced within a Method defined within that same Test Case

def addInventoryItem() {
    log.logWarning('The listed Branch in the quote is: ' + quoteBranchName)
        if (inventoryLocations.contains(quoteBranchName)==false){
        log.logError("The Inventory Item does not match the Branch Location")

This is the first time I've needed to make this kind of reference. The verdict is still out on whether this is "cheating" or not :), but it gets the job, so I'm going with it.

Other articles of interest:

Reading text of the currently selected value from a dropdown list

I was recently working on a test case where I needed to use the default value from a dropdown. For my case, I wanted to know the default location of the user. The dropdown value is set through Javascript and a query. Since it has a default, I didn't want to set it, I wanted to know what that default was. This turned out to be a little more problematic than I thought.

My first attempt was to use .getAttribute which has worked for other fields.

String branchLocation = WebUI.getAttribute(findTestObject('Dropdown Location'),'value') returns
c6f25c57-47a7-4ae3-b269-a57567faa23f which is a GUID and can't be translated into anything I can work with.

I then tried it with getText which does work, but brings back all the options available in the dropdown, not just the currently selected one.

String branchLocation = WebUI.getText(findTestObject('Dropdown Location'))

This returns

Location #1
Location #2
Location #3
Location #4

This is better, but is clearly more information than I want. All I want is the first entry. With that in mind, the String is turned into a List with a split on the CRLF that exists at the end of each line. This gives one entry for each index of the List

String allBranchLocations = WebUI.getText(findTestObject('Dropdown Location'))
List allBranchesList=allBranchLocations.split("\\r?\\n") //Remove CRLF from each dropdown entry
String branchName=allBranchesList[0]

With that little conversion, branchName contains the first item from the dropdown, which is the default value. This can now be used for my comparison. This may not be the best way to get the first item, or perhaps the most reliable, but it works situation and is easier than some of the other solutions I saw offered up.

Other articles of interest:

Recent Comments

  • How to Block games by Title and Tag on Steam (1)
    • JACK: Thanks, same just wanted to block anime games in my discover
  • How To Disable the Quicken Registration Prompt (25)
    • Greg: For me, holding the *LEFT* CTL + Shift then clicking Online, One Step Update worked. I originally tried holding the right CTL + Shift, and it didn’t work. I’m using Quicken 2006, so I don’t know if it will work...
    • Joe SR>: My monthly income is deposited into my Credit Union account. I use debit whenever possible. I write checks manually and mail them. I use Quicken 2012 off-line only. I have entered all my money and investment accounts. I...
    • Prtet: Never say never….every time I swear I will never use Quicken again, I discover that there are still no viable alternatives. Amazing that there is no decent personal finance software.
    • Joe D.: Holding CTRL + Shift keys and selecting On Line | One Step Update from the main menu worked for my Quicken 2004. I’m grateful that you wrote a synopsis (“Simply put, …”) just beneath the link to the blog...
    • Susan Long: I bought my quicken disc in the beginning and it came with a registration number. I rang the helpline and they gave me the code to put in and talked me through it. It you downloaded your version then you don’t own it...
    • Peter: You might consider running your old version of Quicken on an ancient computer. This is what I have done for years. The newer versions are fraught with problems- criminal, in my opinion. One version made mathmatical errors when...
    • Roslyn Chamberlain: Can I stop the countdown in quicken 2001 says only 8 sessions left. and what will happen after?
  • Parsing Strings in Katalon – Split, Substring and Readlines (1)
    • Ellen: Thanks for sharing!! I like your contributions to Katalon topics.
  • Working with Dates and Date Formatting in Katalon Studio (6)
    • Ajoo: Thank you for the details. How do i remove leading “0” from dates. i.e. while formatting i receive 04/21/2019, but i need 4/21/2019. (same applies for date)
  • What is Katalon Studio? A Distro of Selenium, Groovy and Eclipse (1)
    • Mahesh: Looking for more posts on katalon studio.your katalon stuffs are always exiting
  • Simple wildcard searches for pattern matching (2)
    • Don Pedro: For that scenario it seems .contains would be your choice. For example, variable.contains(‘amazo’) to see if the url had In that case,,, would all...
    • Jony: Hi, How can I use a wild card to assert a URL is the one I want. I just want to verify the domain ==expected but not anything after it. Tried * but not working and only works when I have full URL.
  • Create a Dynamic Object at Runtime (2)
    • Saish: How to add shadow root parent to this runtime object..
    • Jeremy Brien: I appreciate this! I saw this post on LinkedIn this morning and was able to find a use case for it! I found that defining my xpath with an iterable variable allows me to loop through and capture text from tables created...