With the Test Automation Noob blog rolling past 17000 page views, I wanted to thank everybody who has asked questions, taken a few moments to send some feedback regarding posts, or recommended the blog to others; hearing from readers has been very gratifying. I also apologize for the sporadic frequency of postings: between work, parenthood (is that redundant?), and other pursuits it's sometimes difficult to carve out time to write new posts on a consistent basis.
If you've looked beyond this blog for soapUI scripting samples, you may have noticed some differences between the scripts here and those posted elsewhere. Some of these are due to differences between Groovy and Java-- frankly, my scripts tend to be "Java-centric" and don't take advantage of many of Groovy's shortcuts; I think some of the Groovy constructs are a little less intuitive and harder to grasp for somebody starting out in scripting, and I came to soapUI with experience in Java, so I never really dove into the nuts and bolts of Groovy. I hope to write a post in the not-too-distant future to bridge some of those gaps and illustrate some of the different ways of doing things in Groovy that can help make your scripts more compact. However, there is one Groovy keyword I haven't used thus far, but intend to use in the future: the def keyword, used when declaring a new variable in a script. If you've already started implementing scripts without def, don't panic! It's optional; the scripts I've written so far should be working fine without it, and omitting it saves a little bit of typing. If you only intend to write simple scripts in soapUI for the odd situation where the standard interface isn't quite sufficient, you can probably get away with leaving out the def keyword. However, moving forward I intend to use the keyword in scripts and you may see some of the past scripts and posts re-worked to include it (look for an update to the first scripting post on variables for more detail on using the def keyword). I have two primary reasons for doing this: first, as I mentioned before, def is ubiquitous in scripting examples elsewhere, including soapUI's own site, so including it here provides some consistency. Secondly, after reading over discussions on the topic and weighing different arguments, I think it's just better practice, particularly if you think you may end up working with Groovy outside of soapUI someday-- getting into the habit of using it now when it may not matter could help avoid headaches later on when it does.
Beginning soapUI Scripting 7: More with the XmlHolder Class
You can download a project containing examples using the XmlHolder class (including the scripts covered in this post) here. While I discuss XPath expressions here, a detailed explanation of how they work is beyond the scope of this post-- if you're unfamiliar with XPath, I highly recommend this tutorial at www.w3schools.com.
In my last post I introduced the XmlHolder class and demonstrated how it can be used with XPath expressions in your scripts. In this post, I'll continue the discussion of the XmlHolder class with a look at some of the other assertion scripts included in the sample project I provided.
Let's actually start with the script assertion for the GetAtomicWeight test request (comments are removed in script excerpts for the sake of brevity); this one represents a basic case:
If you've read my last post, this should all be familiar. The import statement makes the XmlHolder class available in the script, and the next three lines pluck the CDATA section out of the response, putting it into the CDATAHolder XmlHolder object. In line 4 of the main body of the script, the getNodeValue() method returns the value of the AtomicWeight element in the CDATA section, assigning it to the variable atomWeight, and the final line asserts that atomWeight matches the expected value of "1.00797".
Let's move on to a more interesting example in the script assertion for the GetAtoms test case:
Again, the first few lines (up to the creation of the elementValues variable) retrieve the CDATA string in our response XML and put it into an XmlHolder called dataTableXmlHolder. The next lines illustrate a new method of the XmlHolder class, getNodeValues():
The getNodeValues() method finds all matches in the XML it "holds" for a given XPath expression, putting their values into a list. In this particular example, the GetAtoms operation returns all the element names in the periodic table (in a CDATA section); the call to getNodeValues() retrieves the names and puts them into a list called elementValues. A for loop then iterates over the members of the list and prints them to the script log.
This is a call to the familiar getNodeValue() method, but in this case instead of returning the value of a single node we're using XPath's count() function to count the number of elements that match the expression "/NewDataSet//Table"; the result is assigned to the variable countVal.
An assertion confirms the result of the count() function is 112. The getNodeValue() method returns data as a string; here we're converting that string to a different data type. The parseInt() method of Java's Integer class takes a string representation of a number as an argument and converts it to an integer. Note that the method is called against the Integer class itself, not against an object of that class-- compare this to the getNodeValue() method in the previous line, for example, which is called against dataTableXmlHolder, an object of class XmlHolder. Methods like this that are called against a class instead of an instance of an object are called static methods in Java.
The final two lines illustrate using the getNodeValue() method with a more complex XPath expression to confirm a specific XML element or value does not exist in our response. Here the count() function is used to count the number of elements named "Adamantium" in the result set. Since adamantium isn't a real element, we assert that the count should equal 0. Once again we use the parseInt() method to convert the result of the getNodeValue() function to an integer for the purposes of our assertion.
The last assertion script we'll look at comes from the GetElementSymbol test case:
The first few lines are the standard lines to deal with CDATA. In the final two lines, another type of XPath expression is used with the getNodeValue() method; in this case, the expression is a comparison ("//Symbol = 'H'") that evaluates to true or false. Again, since the method always returns its result as a string, we can use another static method, the parseBoolean() method of Java's Boolean class, to convert the result to an actual Boolean value to use in our assertion.
In my last post I introduced the XmlHolder class and demonstrated how it can be used with XPath expressions in your scripts. In this post, I'll continue the discussion of the XmlHolder class with a look at some of the other assertion scripts included in the sample project I provided.
Let's actually start with the script assertion for the GetAtomicWeight test request (comments are removed in script excerpts for the sake of brevity); this one represents a basic case:
import com.eviware.soapui.support.XmlHolder respXmlHolder = new XmlHolder(messageExchange.getResponseContentAsXml()) respXmlHolder.declareNamespace("ns1","http://www.webserviceX.NET") CDATAHolder = new XmlHolder(respXmlHolder.getNodeValue("//ns1:GetAtomicWeightResult")) atomWeight = CDATAHolder.getNodeValue("//AtomicWeight") assert atomWeight == "1.00797"
If you've read my last post, this should all be familiar. The import statement makes the XmlHolder class available in the script, and the next three lines pluck the CDATA section out of the response, putting it into the CDATAHolder XmlHolder object. In line 4 of the main body of the script, the getNodeValue() method returns the value of the AtomicWeight element in the CDATA section, assigning it to the variable atomWeight, and the final line asserts that atomWeight matches the expected value of "1.00797".
Let's move on to a more interesting example in the script assertion for the GetAtoms test case:
import com.eviware.soapui.support.XmlHolder respXmlHolder = new XmlHolder(messageExchange.getResponseContentAsXml()) respXmlHolder.declareNamespace("ns1","http://www.webserviceX.NET") dataTableXmlHolder = new XmlHolder(respXmlHolder.getNodeValue("//ns1:GetAtomsResult")) elementValues = dataTableXmlHolder.getNodeValues("/NewDataSet//Table/ElementName") for (i in elementValues) { log.info(i) } countVal = dataTableXmlHolder.getNodeValue("count(/NewDataSet//Table)") assert Integer.parseInt(countVal) == 112 countAdamantium = dataTableXmlHolder.getNodeValue("count(//Table[ElementName='Adamantium'])") assert Integer.parseInt(countAdamantium) == 0
Again, the first few lines (up to the creation of the elementValues variable) retrieve the CDATA string in our response XML and put it into an XmlHolder called dataTableXmlHolder. The next lines illustrate a new method of the XmlHolder class, getNodeValues():
elementValues = dataTableXmlHolder.getNodeValues("/NewDataSet//Table/ElementName") for (i in elementValues) { log.info(i) }
The getNodeValues() method finds all matches in the XML it "holds" for a given XPath expression, putting their values into a list. In this particular example, the GetAtoms operation returns all the element names in the periodic table (in a CDATA section); the call to getNodeValues() retrieves the names and puts them into a list called elementValues. A for loop then iterates over the members of the list and prints them to the script log.
countVal = dataTableXmlHolder.getNodeValue("count(/NewDataSet//Table)")
This is a call to the familiar getNodeValue() method, but in this case instead of returning the value of a single node we're using XPath's count() function to count the number of elements that match the expression "/NewDataSet//Table"; the result is assigned to the variable countVal.
assert Integer.parseInt(countVal) == 112
countAdamantium = dataTableXmlHolder.getNodeValue("count(//Table[ElementName='Adamantium'])") assert Integer.parseInt(countAdamantium) == 0
The final two lines illustrate using the getNodeValue() method with a more complex XPath expression to confirm a specific XML element or value does not exist in our response. Here the count() function is used to count the number of elements named "Adamantium" in the result set. Since adamantium isn't a real element, we assert that the count should equal 0. Once again we use the parseInt() method to convert the result of the getNodeValue() function to an integer for the purposes of our assertion.
The last assertion script we'll look at comes from the GetElementSymbol test case:
import com.eviware.soapui.support.XmlHolder respXmlHolder = new XmlHolder(messageExchange.getResponseContentAsXml()) respXmlHolder.declareNamespace("ns1","http://www.webserviceX.NET") CDATAHolder = new XmlHolder(respXmlHolder.getNodeValue("//ns1:GetElementSymbolResult")) symbolExists = CDATAHolder.getNodeValue("//Symbol = 'H'") assert Boolean.parseBoolean(symbolExists)
The first few lines are the standard lines to deal with CDATA. In the final two lines, another type of XPath expression is used with the getNodeValue() method; in this case, the expression is a comparison ("//Symbol = 'H'") that evaluates to true or false. Again, since the method always returns its result as a string, we can use another static method, the parseBoolean() method of Java's Boolean class, to convert the result to an actual Boolean value to use in our assertion.
Beginning soapUI Scripting 6: The XmlHolder Class and Working with CDATA
You can download a project containing examples using the XmlHolder class (including the script covered in detail in this post) here. While I discuss XPath expressions here, a detailed explanation of how they work is beyond the scope of this post-- if you're unfamiliar with XPath, I highly recommend this tutorial at www.w3schools.com.
In this post I'll be looking at soapUI's XmlHolder class, which provides methods to work easily with XML data within scripts. As you'll see, adding it to your toolbox can open up a lot of new and very useful possibilities, including the ability to mimic XPath assertions. You may be asking yourself why you'd want to do that-- XPath assertions are included as a standard assertion type, after all, and shouldn't require any additional scripting. But if you've looked at any of my extended tutorials working with the PeriodicTable sample web service, you've actually seen one scenario where the XmlHolder class could come in handy: working with CDATA.
Below is a screenshot from soapUI that shows the response to a test request to the GetAtomicNumber operation of the aforementioned PeriodicTable web service. The operation takes the name of an element (hydrogen in this case) as an argument and responds with information about certain properties of that element-- including, of course, its atomic number:
A response from the GetAtomicNumber operation
The XML block containing information about hydrogen is returned as a CDATA section-- it's a string that's returned as the value of the GetAtomicNumberResult node. Consequently, if you try to create a single standard XPath assertion looking for any of the nodes contained within that CDATA section (if you try to assert that the value of the MeltingPoint element is 14, for example) you'll run into problems.
You could just deal with this with a basic Contains assertion, looking for the MeltingPoint node as a string. Since the Contains assertion looks at the entire response as a string, the issues related to parsing XML with a CDATA section are bypassed. However, you may have a particular situation where you can't or don't want to use a Contains assertion.
SoapUI's site provides a detailed explanation of how to deal with this scenario here. In a nutshell, you can use one XPath expression to isolate the CDATA section, effectively stripping off the rest of the XML in which it's embedded. Once isolated, you can use further XPath expressions on the XML in the CDATA section.
Scripting may not be necessary in some scenarios where you can use two XPath expressions together (in a Property Transfer step, for example, where you can specify multiple transfers). If you're trying to create an assertion against a test request that uses CDATA, however, some scripting will probably be required-- and the methods provided by the XmlHolder class can make things a whole lot easier.
Let's jump right into a short script. If you've downloaded the sample project, this is the Groovy assertion for the GetAtomicNumber test request test step (comments are excluded here); it tackles the problem I gave above as an example: verifying that the value of the MeltingPoint XML element is 14:
Let's look at this script line by line:
The XmlHolder class is not included with soapUI's core components; therefore, this line is required to import it from its package, the com.eviware.soapui.support package, for use in the script.
This line creates a new XmlHolder object and assigns it to the variable respXmlHolder. The XmlHolder constructor (the method used to create our new XmlHolder object-- the constructor has the same name as the class itself and is preceded by the
In these lines we're using our XmlHolder object (respXmlHolder) to use an XPath expression with the response XML. The first line declares a namespace to use in our XPath expression, assigning the URI "http://www.webserviceX.NET" to the prefix "ns1". In the next line, the XPath expression itself is passed as an argument (a string) to the getNodeValue() method of the XmlHolder object; note how the XPath expression uses the namespace prefix we just defined in the previous line:
At this point we've basically isolated the CDATA section of our original XML response; now we can use that data with XPath expressions.
These lines are similar to preceding lines, repeating many of the same actions. In the first line we create another XmlHolder object, this time using the variable CDATAXml as the argument to its constructor, and assign it to variable CDATAXmlHolder. In the second line, we use the getNodeValue() method to evaluate the XPath expression
Finally, we have our assertion:
The assertion checks to make sure the value we previously retrieved from the MeltingPoint element matches its expected value. There's one crucial thing to note here: the getNodeValue() method returns a string value. This is taken into account in our assertion, which checks that the value of meltPoint matches "14"-- the string representation of the number 14-- instead of a pure numeric representation.
One final note: I see a NullPointerException when the getNodeValue() method is used in an assert statement and the assertion fails (there's no error if the assertion passes). I'm not sure if this is an as yet unidentified issue with my script or a minor bug in soapUI (I'm using version 4.5.1). Passing the result of the getNodeValue() method to a variable and then using that variable with the assert statement (as I did in the example above) seems to avoid the error.
I'll look at some key points in the remaining assertion scripts in the example project in my next post.
In this post I'll be looking at soapUI's XmlHolder class, which provides methods to work easily with XML data within scripts. As you'll see, adding it to your toolbox can open up a lot of new and very useful possibilities, including the ability to mimic XPath assertions. You may be asking yourself why you'd want to do that-- XPath assertions are included as a standard assertion type, after all, and shouldn't require any additional scripting. But if you've looked at any of my extended tutorials working with the PeriodicTable sample web service, you've actually seen one scenario where the XmlHolder class could come in handy: working with CDATA.
The CDATA Problem
A response from the GetAtomicNumber operation
The XML block containing information about hydrogen is returned as a CDATA section-- it's a string that's returned as the value of the GetAtomicNumberResult node. Consequently, if you try to create a single standard XPath assertion looking for any of the nodes contained within that CDATA section (if you try to assert that the value of the MeltingPoint element is 14, for example) you'll run into problems.
You could just deal with this with a basic Contains assertion, looking for the MeltingPoint node as a string. Since the Contains assertion looks at the entire response as a string, the issues related to parsing XML with a CDATA section are bypassed. However, you may have a particular situation where you can't or don't want to use a Contains assertion.
SoapUI's site provides a detailed explanation of how to deal with this scenario here. In a nutshell, you can use one XPath expression to isolate the CDATA section, effectively stripping off the rest of the XML in which it's embedded. Once isolated, you can use further XPath expressions on the XML in the CDATA section.
Scripting may not be necessary in some scenarios where you can use two XPath expressions together (in a Property Transfer step, for example, where you can specify multiple transfers). If you're trying to create an assertion against a test request that uses CDATA, however, some scripting will probably be required-- and the methods provided by the XmlHolder class can make things a whole lot easier.
Sample Script Using the XmlHolder Class
import com.eviware.soapui.support.XmlHolder respXmlHolder = new XmlHolder(messageExchange.getResponseContentAsXml()) respXmlHolder.declareNamespace("ns1","http://www.webserviceX.NET") CDATAXml = respXmlHolder.getNodeValue("//ns1:GetAtomicNumberResult") log.info(CDATAXml) CDATAXmlHolder = new XmlHolder(CDATAXml) meltPoint = CDATAXmlHolder.getNodeValue("//MeltingPoint") log.info("Melting Point = $meltPoint") assert meltPoint == "14"
Let's look at this script line by line:
import com.eviware.soapui.support.XmlHolder
The XmlHolder class is not included with soapUI's core components; therefore, this line is required to import it from its package, the com.eviware.soapui.support package, for use in the script.
respXmlHolder = new XmlHolder(messageExchange.getResponseContentAsXml())
This line creates a new XmlHolder object and assigns it to the variable respXmlHolder. The XmlHolder constructor (the method used to create our new XmlHolder object-- the constructor has the same name as the class itself and is preceded by the
new
keyword) takes a string of XML as an argument. In this case, the XML we're interested in getting is our response data-- and conveniently, soapUI provides us with the built-in messageExchange variable in script assertions. We can use this variable's getResponseContentAsXml() method to quickly get our test request's response XML (as a string). We end up with an XmlHolder object that contains and can act on our response XML.respXmlHolder.declareNamespace("ns1","http://www.webserviceX.NET") CDATAXml = respXmlHolder.getNodeValue("//ns1:GetAtomicNumberResult") log.info(CDATAXml)
In these lines we're using our XmlHolder object (respXmlHolder) to use an XPath expression with the response XML. The first line declares a namespace to use in our XPath expression, assigning the URI "http://www.webserviceX.NET" to the prefix "ns1". In the next line, the XPath expression itself is passed as an argument (a string) to the getNodeValue() method of the XmlHolder object; note how the XPath expression uses the namespace prefix we just defined in the previous line:
//ns1:GetAtomicNumberResult
. This XPath expression returns the value of the GetAtomicNumberResult element in the response XML, which is the CDATA section. The result of the evaluation is assigned to the variable CDATAXml and (for informational purposes only) the contents of CDATAXml are written to the script log-- you should see a string of XML in the log starting (and ending) with a "NewDataSet" tag:At this point we've basically isolated the CDATA section of our original XML response; now we can use that data with XPath expressions.
CDATAXmlHolder = new XmlHolder(CDATAXml) meltPoint = CDATAXmlHolder.getNodeValue("//MeltingPoint") log.info("Melting Point = $meltPoint")
These lines are similar to preceding lines, repeating many of the same actions. In the first line we create another XmlHolder object, this time using the variable CDATAXml as the argument to its constructor, and assign it to variable CDATAXmlHolder. In the second line, we use the getNodeValue() method to evaluate the XPath expression
//MeltingPoint
. The result of the evaluation is assigned to the variable meltPoint and written to the script log:Finally, we have our assertion:
assert meltPoint == "14"
The assertion checks to make sure the value we previously retrieved from the MeltingPoint element matches its expected value. There's one crucial thing to note here: the getNodeValue() method returns a string value. This is taken into account in our assertion, which checks that the value of meltPoint matches "14"-- the string representation of the number 14-- instead of a pure numeric representation.
One final note: I see a NullPointerException when the getNodeValue() method is used in an assert statement and the assertion fails (there's no error if the assertion passes). I'm not sure if this is an as yet unidentified issue with my script or a minor bug in soapUI (I'm using version 4.5.1). Passing the result of the getNodeValue() method to a variable and then using that variable with the assert statement (as I did in the example above) seems to avoid the error.
I'll look at some key points in the remaining assertion scripts in the example project in my next post.
soapUI: Scripting Objects - MessageExchange Interface
This is a continuation of a series of posts attempting to review some of the objects (and methods) you're likely to encounter when you start scripting in soapUI. To see the full soapUI javadoc, click here.
SoapUI's MessageExchange interface defines methods for retrieving information related to test requests, including request and response data. Within a Groovy Script assertion, a built-in variable (messageExchange) provides access to an object of a class that implements the MessageExchange interface. Several step result classes (including the WsdlTestRequestStepResult class) also implement the MessageExchange interfaces. Some of the common methods of the interface (available to objects of all classes that implement the interface) include:
- getEndpoint() : returns the target endpoint as a String
- getRequestContent() : returns the content of the test request
- getRequestContentAsXml() : returns the content of the test request (as XML)
- getResponseContent() : returns the content of the response to the test request
- getResponseContentAsXml() : returns the content of the response to the test request (as XML)
- getTimeStamp() : returns the request time stamp as a long number (see below for an example of converting from long to a standard time format)
- getTimeTaken() : returns the amount of time taken to process the request (in milliseconds)
- getRequestHeaders() : returns request headers as a StringToStringsMap object
- getResponseHeaders() : returns response headers as a StringToStringsMap object
A map (as implemented in the StringToStringsMap objects returned by the getRequestHeader() and getResponseHeader() methods) is similar to a list in that it's capable of storing multiple data items. Map "entries" are stored as data pairs: a key and a value. So while an item in a list is retrieved using its location in the list order (its index), a data value in a map is retrieved using its associated key. If that explanation still unclear, see the example below using the getResponseHeaders() method, which illustrates retrieving data from a StringToStringsMap object.
These coding examples use the PeriodicTable service I've used in many other posts and reflect a test request call made to its GetAtomicNumber operation. The first example illustrates some of the methods illustrated above as they might be used in a Groovy Script assertion, called using the script assertion's built-in messageExchange variable:
The script log output looks something like this:
As I mentioned above, you're not restricted to using these methods with the messageExchange variable in script assertions-- you can use them with any class that implements the MessageExchange interface. Here's another example taken from a Groovy Script step that uses a WsdlTestRequestStep object to invoke some of the methods:
And the expected output:
Common MessageExchange Methods
- getEndpoint() : returns the target endpoint as a String
- getRequestContent() : returns the content of the test request
- getRequestContentAsXml() : returns the content of the test request (as XML)
- getResponseContent() : returns the content of the response to the test request
- getResponseContentAsXml() : returns the content of the response to the test request (as XML)
- getTimeStamp() : returns the request time stamp as a long number (see below for an example of converting from long to a standard time format)
- getTimeTaken() : returns the amount of time taken to process the request (in milliseconds)
- getRequestHeaders() : returns request headers as a StringToStringsMap object
- getResponseHeaders() : returns response headers as a StringToStringsMap object
A map (as implemented in the StringToStringsMap objects returned by the getRequestHeader() and getResponseHeader() methods) is similar to a list in that it's capable of storing multiple data items. Map "entries" are stored as data pairs: a key and a value. So while an item in a list is retrieved using its location in the list order (its index), a data value in a map is retrieved using its associated key. If that explanation still unclear, see the example below using the getResponseHeaders() method, which illustrates retrieving data from a StringToStringsMap object.
Examples Using MessageExchange Methods
log.info("MessageExchange class = " + messageExchange.getClass().toString()) log.info("Endpoint = " + messageExchange.getEndpoint()) log.info("Request Content = " + messageExchange.getRequestContent()) log.info("Response Content Xml = " + messageExchange.getResponseContentAsXml()) log.info("Time Taken = " + messageExchange.getTimeTaken() + "ms") //Using a map: get the map and assign it to variable respHeadersMap respHeadersMap = messageExchange.getResponseHeaders() //Retrieve the map's keys and assign them to a list, respHeadersKeys respHeadersKeys = respHeadersMap.getKeys() log.info("Response Headers info:") /*Use a for loop to step through each key in the map and use the map's get function to get each key's corresponding map data. The get function takes two arguments: the first is the key for which you'd like the corresponding data, the second is a default value if the key can not be found in the map.*/ for(key in respHeadersKeys){ log.info(" " + key + ": " + respHeadersMap.get(key,"NONE")) } //To clarify, here's the Date header retrieved again: log.info(" Date (again): " + respHeadersMap.get("Date","NONE")) /*And here's a call to retrieve from a key that doesn't exist; the default value of "NONE" is used instead.*/ log.info(" Bogus: " + respHeadersMap.get("Bogus","NONE"))
The script log output looks something like this:
As I mentioned above, you're not restricted to using these methods with the messageExchange variable in script assertions-- you can use them with any class that implements the MessageExchange interface. Here's another example taken from a Groovy Script step that uses a WsdlTestRequestStep object to invoke some of the methods:
//Get the result for the first step, a test request step //Assign it to variable tReqResult tReqResult = testRunner.getResults()[0] log.info("tReqResult Class = " + tReqResult.getClass().toString()) log.info("Endpoint = " + tReqResult.getEndpoint()) log.info("Request Content Xml = " + tReqResult.getRequestContentAsXml()) log.info("Response Content = " + tReqResult.getResponseContent()) //Assign the value returned by getTimestamp() to variable timeStampAsLong timeStampAsLong = tReqResult.getTimestamp() //Use long value stored in timeStampAsLong to create a new Date object, assigned //to variable timeStampAsTime; this can be used to get a "friendly" date format. timeStampAsTime = new Date(timeStampAsLong) log.info("Time Stamp = " + timeStampAsTime)
And the expected output:
Subscribe to:
Posts (Atom)