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.

The CDATA Problem

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.

Sample Script Using the XmlHolder Class

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:

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.

21 comments:

  1. Hi
    After reading your post, i am little confused with XmlHolder and xmlslurper. Please help me to understand. i have a xml response as below and have to validae my response has the is lesser than and greater than some date in all sections. How can i consult this logic.


    ...
    .....date1
    ...
    ...
    .....date2
    ...
    ...
    .....date3
    ...

    ReplyDelete
    Replies
    1. Full disclosure: I don't have too much experience with the XmlSlurper object, but I did take a quick look at some of the documentation and tried playing around with it a little.

      In response to your first question (the difference between the two): XmlSlurper is a class that helps you work with XML like the XmlHolder class, but the XmlHolder class is provided with soapUI (XmlSlurper is a Groovy class). Looking at the description of the class and sample code, I'm assuming you're looking at using the parseText() function of the XmlSlurper class with the XML you want to work with-- that actually returns a GPathResult object, which seems to be an object that mirrors the structure of the XML-- it has properties that correspond to the various nodes of the XML fed to the parseText() function, so you traverse the XML by working with the various properties of the GPathResult object. The XmlHolder class is a little different; it just provides methods to work with the XML it "holds", typically with XPath expressions. There are probably a lot of cases where you can achieve the same functionality using either class.

      As to the strategy you might want to use for your particular situation; you might be able to do it by getting those date nodes together into a list, and then iterating through the list, asserting that each item is greater than or less than your target date. With the XmlHolder class, you can do that with the getNodeValues() method, using an XPath expression corresponding to the date nodes-- the getNodeValues() function finds all nodes that match the expression and puts them into a list.

      The list is a list of strings-- so when iterating over the list, you'd have to convert those strings to dates before you could assert whether they were greater than or less than another date. For information on how to do this, I suggest you check out the post at http://www.mkyong.com/java/how-to-convert-string-to-date-java/ .

      Given an XML tree like this (in the code below, the variable dateXmlString refers to this XML as a string):

      <Root>
      <GetEventsResults>
      <Event>
      <Name>...</Name>
      <Date>...</Date>
      </Event>
      <Event>
      <Name>...</Name>
      <Date>...</Date>
      </Event>
      (... etc.)
      </GetEventsResults>
      </Root>


      Here's an example of the code you might be able to use to confirm all the dates are greater than today's date (this code assumes the dates in the XML are in "dd-MM-yyyy" format:

      import com.eviware.soapui.support.XmlHolder
      import java.text.SimpleDateFormat //Used to convert string to date

      xmlHolder = new XmlHolder(dateXmlString)
      eventDates = xmlHolder.getNodeValues("//Date")
      def myDateFormat = new SimpleDateFormat("dd-MM-yyyy")
      def todaysDate = new Date()
      eventDates.each{assert myDateFormat.parse(it) > todaysDate}

      Delete
    2. now i have understood the difference between those two. thanks much.

      Delete
  2. The link to the example project file above may have been pointing to an incorrect version. Hopefully, I've resolved this issue; if you have an example project that doesn't quite look right (the incorrect version contains sections of commented out code) please try re-downloading the project file from the link above.

    ReplyDelete
  3. hi. i have read all you posts and follow you for future posts. would like to check is there any option to download all your posts, so that we can have it offline also to refer whenever we need.

    ReplyDelete
    Replies
    1. Unfortunately, at the moment I have no option to do that. I've been weighing the possibility of putting some or all of this into an e-book format, but I wasn't sure if there'd be any interest-- thanks for your comment, which confirms that there may be some interest out there after all... I'll start trying to get together an off-line version, but it may take some time.

      Delete
    2. Wow!!! great news. we will wait for the day.

      Delete
  4. Hi,

    I am having a problem in groovy. I Need to know that how can we parse a string into the object array.
    For eg. -

    If i make a search for mobilePhones i get a response as string which is habing multiple mobilephone records in Serailized form.
    i need to first deserialize the response then also need to pick one of the mobilephone response as input for the next step.

    class MobilePhone
    {
    string companyName;
    string modelNumber;
    }

    i make a search request of mobile phones and i get the response as

    ABCGT62ABCGT64ABCGT65

    This is the serialized form of array in the string format.

    Now to get details i need top pick one of the mobile phone object and send it to the next step..

    Please help me out Acc to me it can be done with groovy but i am unable to do the same.

    ReplyDelete
    Replies
    1. Honestly, this issue sounds like it may be beyond my abilities, but I'd be glad to take a look, if you're not having any luck elsewhere-- you may also want to try the soapUI forum or even stackoverflow if you haven't tried those sites already. Could you provide some more information by way of concrete examples? In particular, could I see what the response looks like in XML (or JSON?), the code you're trying to use to get the data, and a sample request for the next step? This isn't a public free service you're using, I take it. It may be overkill to post all that here in the Comments section; if you'd like, you can send me your info via the Contact Me form and we can follow up via email.

      Delete
  5. Please help me on the below one

    My response XML are like that


    UPSN
    1Z75Y7E00409342053


    UPSN
    1Z75Y7E00409342053


    UPSN
    1Z75Y7E00409342106


    UPSN
    1Z75Y7E00409342106


    UPSN
    1Z75Y7E00409342115

    I need to store all the TrackingNumber from my response XML.

    I am using the below code but getting an error:

    import com.eviware.soapui.support.XmlHolder

    def groovyUtils = new com.eviware.soapui.support.GroovyUtils( context )
    holder.namespaces["ns2"] = "http://service.nordstrom.net/ordersearch/SVOrderSearchService/v2/"
    def holder = groovyUtils.getXmlHolder("TestStepName#FindChargeSendDetail#FindChargeSendDetailResponse")
    log.info holder.getNodeValue("//ns2:TrackingNumber")
    log.info holder['//ns2:TrackingNumber']

    Please help me on this

    ReplyDelete
    Replies
    1. What kind of error are you seeing and do you have any idea which line is throwing the error? One thing I see is that you seem to be trying to use the holder variable (holder.namespace["ns2"]...) before you've actually defined it (in the next line). And is that property string you're using in the getXmlHolder call correct?

      Where are you trying to run this code? I've tried running something similar and the results don't look quite right-- running the code as part of a script assertion on the test request in question, using def holder = new XmlHolder(messageExchange.responseContentAsXml) to create your holder seems to work, however.

      Delete
  6. Hi, how can i do that?
    i have one request and i use the property transfer for send a cdata value at the request 2 and now y want to get that data in a groovy script to manipulaty like i want, the problem is the method:

    import com.eviware.soapui.support.XmlHolder
    def holder = new XmlHolder( messageExchange.responseContentAsXml )

    only works in script assertions and don't works for me.

    any help? ty and sorry for my english.

    ReplyDelete
    Replies
    1. I think I understand your question: the code snippet above assumes the code is being run as part of a Groovy assertion, where the messageExchange variable is available. But you're trying to do the same thing from a Groovy test step after some other request step and would like to get at the request step's response data-- the messageExchange variable is NOT available in this case.

      You can get at the response content for a previous request test step through its result object, which itself is accessible through the testRunner variable (I'm assuming here that your Groovy step and request step are both in the same test case). If you put your Groovy step immediately after your request step, you can do something like this:

      def lastRequestResult = testRunner.getResults()[-1] //the index of -1 grabs the result for the last step that was run
      def holder = new XmlHolder(lastRequestResult.responseContentAsXml)

      If your Groovy Step isn't immediately after your request step, you can adjust the index used accordingly-- e.g., -2 if the step is two steps before your Groovy Step, -3 if it's 3 steps before, etc.

      Hope that helps.

      Delete
    2. Actually i'm using the SoapUI 5.1.3 free version and in the 5.1.2 api for testRunner there aren't any method called getResults() https://www.soapui.org/apidocs/com/eviware/soapui/model/testsuite/TestRunner.html
      and i try your code and the console throw me one error, IndexOutOfBoundExeption: Negative array index[-1] too large for array size 0

      Ty for your help i think i'm a little bad with groovy actually, but i put that in the code:
      import com.eviware.soapui.support.XmlHolder

      def lastRequestResult = testRunner.getResults()[-1]
      def holder = new XmlHolder(lastRequestResult.responseContentAsXml)

      and throw me the error i forget something?

      Delete
    3. It looks like the error message is saying there aren't any results yet for testRunner. How exactly is your project structured? Where are you putting this code, and where is the test request step for which you're trying to get the response content? I would expect this code to work in a scenario where it's running in a Groovy Script Step immediately after a test request step with both steps contained in the same test case. Are your steps (the request step and Groovy step) in the same test case in your project? This error message looks like maybe your Groovy Script step is running as the first or only step in its test case.

      Delete
    4. Well i try to explain it:
      i have one Test Suite, in that i have 5 Test Case, actually i'm working only with one, and that one have 4 Test Steps in that order:
      SOAP Request1: it's the basic auto created by SOAP-UI and in that have the XML where i put inside the tag my test (in a CDATA because is a XML and i don't want SOAP interprete it like XML, just like a String).
      Property Transfer: there i have 2 Transfer, one for send the result CDATA from SOAP Request 1 to mi global property (it was create before) and the other Transfer just send from the global property to the SOAP Request 2 the value i want with XPATH.
      SOAP Request 2: it's basic i create with Add Step -> SOAP Request and i select for that Request the function with work in the web services.
      And Finally one Groovy Script: there i want to get the data from the tag in the second request.

      Your last idea was good but the problem is the second request couldn't be execute because throw error because soap will try to throw it to the web service and that parameter isn't valid and stop the execution.

      I think you want this and i hope i express correctly, really thanks a lot for that advice.

      Delete
    5. Why don't you try reaching out to me through the contact me link? There are still some things I don't understand and I think some screenshots might be helpful. From the error above, the Groovy Step seems to be running, but there aren't any results accumulated for the test case. If all of these steps were within one test case I don't see how that could be-- even if the second request fails, a fail is still a result, and you should also still have results from the preceding steps. The Groovy step might be expected to throw an error if request 2 fails to execute, but it seems like it should be a different error.

      Delete
    6. thanks a lot but my mate show me Hudson and i try to do with that, he said is more simple because i can access to the data very easy.

      Delete
  7. Hi, i have one property and it saves a id, now i want to put that property into a CDATA like that:










    "HERE THE PROPERTY VALUE"


    ]]>





    im try it using a property transfer but i can acces throw xpath to the CDATA fields, any suggestion? ty a lot.

    ReplyDelete
    Replies
    1. I have to confess I've never tried something like what you're trying to do. But my first thought would be to try building the entire CDATA block instead of trying to plug in the id property by itself. So, for example, you could get the id and save it to a property. In a subsequent Groovy step, build a string corresponding to the entire CDATA block you'll eventually want to plug into the request-- using the id property you just saved-- and save that string into its own property. Then try plugging that CDATA string property into the request element using property expansion (e.g., "${#TestCase#myIdAsCDATA}"). I'm not sure how you're organizing your tests (are you getting the Id and using it all within one test case?), so it's up to you to decide where you'd set up all these properties (context, test case, test suite, etc.).

      Delete
  8. Whenever there is a thing where i Stucked in SOAP UI....your blog came as a Savior ...Thanks Once again ..God Bless you!!

    ReplyDelete

Please be respectful of others (myself included!) when posting comments. Unfortunately, I may not be able to address (or even read) all comments immediately, and I reserve the right to remove comments periodically to keep clutter to a minimum ("clean" posts that aren't disrespectful or off-topic should stay on the site for at least 30 days to give others a chance to read them). If you're looking for a solution to a particular issue, you're free to post your question here, but you may have better luck posting your question on the main forum belonging to your tool's home site (links to these are available on the navigation bar on the right).