Writing Groovy Script Libraries in SoapUI
Very often, when creating tests in SoapUI, we end up writing a lot of Groovy code to add extra flexibility to our tests, create central or shared assertions, load files and data from disk or simply add an extra level of reusability to our tests. So this article is specially targeted towards API testers working with the SoapUI tool by SmartBear, enabling them to reuse their Groovy code in various projects without resolving to the developer-hated copy-paste technique.
Although SoapUI is a nice API testing tool that has quite extensive Groovy scripting support, it is not essentially designed to be used as a platform that facilities code development but rather as a declarative environment that enables test engineers to declare Test Cases and assertions quickly. However, resolving to script capabilities is often inevitable when we have to load test data from a database, CSV files, etc., without resolving to the paid version of SoapUI. In a multi-project environment, in such cases, very often engineers have to resolve to copy+past technique when they want to apply the same Groovy code on different SoapUI projects. One way to work around this problem is to write the shared code into the form of JAR library that can then be imported into different projects. However, this can be very impractical and demanding if our code references specific SoapUI features or session information. So here we will see how to create shared code in a more SoapUI native way.
Developing shared Groovy Libraries
The process of developing shared SoapUI libraries can be divided into the following steps:
- Creating a shared project with common functionality,
- Creating Groovy classes with library methods,
- Defining library loading code in a project that is going to use a shared library,
- Using shared library code in specific Groovy Test Step.
1. Shared Project
The first step in developing a shared Groovy script library in SoapUI is to create an ordinary SoapUI project that will contain one or more Test Suites that will contain one or more Test Cases composed of Groovy Test Steps.
Fig. 1. Example creating a shared project
2. Library Groovy classes
When we have defined Groovy Test Steps within the SoapUI project that we wanted to share, we have to define a Groovy class containing methods that we want to reuse. We define one class for each Groovy Test Step we want to share. Class has to have a constructor that accepts: log, context, and testRunner parameters. These parameters are actually global objects defined by SoapUI and made available to each Groovy Test Step.
An example of such a constructor can be seen here:
/**
* Common utility functions library
*/
class CommonUtils {
//Global objects
def log
def context
def testRunner
def CommonUtils(log, context, testRunner) {
this.log = log
this.context = context
this.testRunner = testRunner
}
}
This constructor gets SoapUI global objects when initialized and used within another Groovy Test Step.
Now we can define one or more reusable methods, and finally, we have to define an initialization block at the Groovy Test Step end, like in this example:
CommonUtils initObj = context.getProperty("CommonUtils")
if (initObj == null) {
initObj = new CommonUtils(log, context, context.getTestRunner())
context.setProperty(initObj.getClass().getName(), initObj)
}
This block of code first checks if the object has already been initialized to assure singleton within the calling context, and if the object hasn’t been initialized then the object is created and registered as a property of the global SoapUI context object. SoapUI generates log, context, and testRunner objects for each test run.
So complete Groovy Test Step that contains a reusable Groovy class with public methods can look like this:
import java.util.zip.GZIPInputStream
/**
* Common utility functions library
*/
class CommonUtils {
//Global objects
def log
def context
def testRunner
def CommonUtils(log, context, testRunner) {
this.log = log
this.context = context
this.testRunner = testRunner
}
/**
* Function that is decompressing compressed XML messages
*/
String unzip(byte[] raw, String encoding = "UTF-8") {
if (raw.length == 0) return ""
String asString = new String(raw)
def idx = asString.indexOf("<soap11:Envelope")
if (idx >= 0) {
return asString.substring(idx);//not zipped
}
idx = asString.indexOf("<Envelope")
if (idx >= 0) {
return asString.substring(idx);//not zipped
}
idx = asString.indexOf("<soapenv:Envelope")
if (idx >= 0) {
return asString.substring(idx);//not zipped
}
for (int i = 0; i < raw.length; i++) {
if (raw[i] == 31) {
def length = raw.size()-1
def messagePart = raw[i..length]
def inflaterStream = new GZIPInputStream(new ByteArrayInputStream(messagePart.toArray(new byte[messagePart.size()])))
def uncompressedStr = inflaterStream.getText(encoding)
log.info "DEBUG: unzip message uncompressedStr=$uncompressedStr"
return uncompressedStr;
}
}
return "Error unzipping message"
}
}
/* Initialisation block */
CommonUtils initObj = context.getProperty("CommonUtils")
if (initObj == null) {
initObj = new CommonUtils(log, context, context.getTestRunner())
context.setProperty(initObj.getClass().getName(), initObj)
}
3. Library loading code
Now that we have defined the class and initialization block, we need a code that will run that block of code from another SoapUI project and thus load classes that the current project will use. The current project shared library object loading block is best placed in its own disabled Groovy Test Step that can be placed in a specific Initialisation Test Case. This Test Case can contain initialization code for both local shared library code and global one.
An example of this organizational structure for the SoapUI project can be seen in the following screenshot:
Fig. 2. Structure of the SoapUI project that uses a shared library
The code in the InitLib Groovy Test Step gets the correct SoapUI Workspace by using a globally available testRunner object. From that workspace object, the script is trying to get a shared library project object. The code works differently when the script is invoked from within the SoapUI GUI environment, or when the script is invoked from the command line, or when running on the Jenkins server without GUI.
After assuring that the shared library project has been correctly opened (here again, the process is different when working within GUI or from the command line), the script executes the shared library initialization block (see above) and thus effectively creating a shared library class and storing the reference to it as a property of the context object.
The whole library loading code is given here:
import com.eviware.soapui.impl.wsdl.WsdlProject
import java.util.HashMap
import java.util.Map
/* LIBRARY INTIALIZATION BLOCK */
def project = null
def workspace = testRunner.testCase.testSuite.project.getWorkspace();
//Defining initialization steps to run
def scriptLibNames = ["ErrorHandling", "CommonUtils"]
Map<String, Object> scriptLibrary = new HashMap<>()
//if running Soapui
if(workspace != null){
project = workspace.getProjectByName("ScriptLibrary")
}
//if running in Jenkins
else{
project = new WsdlProject("src/test/soapui/EMIF-library.xml");
}
if(!project.open) {
project.reload()
//if running Soapui
if(workspace != null){
project = workspace.getProjectByName("ScriptLibrary")
}
//if running in Jenkins
else{
project = new WsdlProject("src/test/soapui/EMIF-library.xml");
}
}
//make a connection to the ScriptLibrary project
if(project.open && project.name == "ScriptLibrary" ) {
def lib = project.getTestSuiteByName("library").getTestCaseByName("common")
if(lib == null) {
throw new RuntimeException("Could not locate ReusableScripts! ");
}
else{
scriptLibNames.each() { stepName ->
testStep = lib.getTestStepByName(stepName)
testStep.run(testRunner, context)
scriptLibrary.put(stepName, context.getProperty(stepName))
//log.warn "Putting ${stepName} with object" + context.getProperty(stepName)
}
}
}
else{
throw new RuntimeException("Could not find project 'ScriptLibrary' !")
}
/* Storing reference to map with all loaded obects as a property of Context object */
scriptLibraryChk = context.getProperty("scriptLibrary")
//log.info scriptLibraryChk
if (scriptLibraryChk == null) {
context.setProperty("scriptLibrary", scriptLibrary)
//log.info scriptLibrary
}
4. Using shared library
Finally, in our project specific Groovy Test Step we can start using our shared library by invoking the following code:
if (context.getProperty("scriptLibrary") == null)
testRunner.testCase.testSuite.getTestCaseByName("Initialization").getTestStepByName("InitLib").run(testRunner, context)
def commonUtils = context.getProperty("scriptLibrary").get("CommonUtils")
def originalResponse = commonUtils.unzip(testRunner.testCase.getTestStepByName(stepName).getTestRequest().messageExchange.rawResponseData)
This code first checks if the library has been loaded already (to assure singleton within the calling context), and then it loads the objects by running a shared library loading script that initializes all shared library objects and stores them in a map object that is available as a property of the context object.
Conclusion
Unfortunately, SoapUI Groovy support does not allow for easy Groovy code editing, debugging, and not to mention Groovy library creation. Thus, we have to stick to workarounds that use Groovy code to invoke other scripts that are residing in different projects within the same workspace. This workaround is not easy or straightforward to implement, but nevertheless, it allows us to have better Groovy script code reusability and, thus less error-prone code with minimal duplication.