My SoapUI Thoughts on Effective Testing in SoapUI

Virtualized Services

There’s been a lot of commentary around the subject of virtualized services recently. As software projects become more complex, businesses have been looking for ways to conduct the development of their software components in parallel. Virtualization is a way for you to build tests for a web service before the service has been implemented - as long as you have the definition of the interface.

SoapUI supports the testing of a virtualized service by means of mocks - a service definition that allows calls to be made to a set of interface stubs prior to their implementation.

In this article, I show how to set up a virtualized service in SoapUI and an easy way to create a family of sample REST requests.

Prerequisites

Goals

  • Knowing how to implement a virtualised service using REST methods
  • Knowing how to make SQL calls from within a groovy script
  • Knowing how to configure the mock responses to return data in JSON or XML formats
  • An awareness of the limits of virtualised services

The Virtualized Methods

The JSONPlaceholder API offers the ability to retrieve records either uniquely by ID or en masse via the resource name. You can ask for a particular user or for every user on the system.

The API doesn’t offer the ability to filter records on partial matches. If for example you wanted to get all the posts whose titles started with a particular letter, you’re out of luck. While you’re waiting for the developers to implement the methods to do this, mocks in SoapUI allow you to design your tests so that you’ll be ready when the methods are available.

Updating the Service Definition

To do this, you’ll need to add the new methods to the JsonPlaceholder service definition, almost as if they have already been implemented. Add a new resource called Mock Requests with a {resourceName} template parameter to the service definition like this.
MockRequests On this resource, create a GET method called GetByTitle. Add a request called request - JSON to the method and configure it as follows. requestJSON

Note in particular that you must override the value in the Endpoint field - http://localhost:8089. This signifies that the mock will run against port 8089 on your local machine, rather than attempting to call the jsonplaceholder API remotely.

Create a second GET method called GetByName with a request called request - XML, configured as follows. requestJSON Your updated service definition should now look like this. SerivceDefinition

Creating a Test Case and Test Step

With the service definition in place, the next task is to create test steps to call the REST methods. If you’ve already created the test suite discussed in the database article, you can create a new test case in this suite called FilterByParameter. Otherwise creating a new test suite as a holder for this test case is fine.

Create a new REST test step in this test case called GetPostsByTitle, using the request - JSON method created in the service definition. NewRESTrequest Update the title parameter with the value ‘qua’ so that the completed test step looks like this. GetPostsByTitle Check that the Endpoint refers to localhost rather than to the JsonPlaceholder site.

For the call to the comments resource, create a second new REST test step called GetCommentsByName, using the request - XML method created in the service definition. Update the name parameter with the value ‘id’ so that the completed test step looks like this. GetCommentsByName As with the first test step, check that the Endpoint refers to localhost rather than to the JsonPlaceholder site.

The next task is now to create the mock service so that these test steps will have something to call.

Creating the Mock Service

Right-click the name of the project and select New REST MockService from the context menu. Call the new mock service Virtualized JSonPlaceholder and add a new Mock Action to it called FilterByParameter. To the FilterByParameter action, add two mock responses called PostsByTitle and CommentsByName. Your mock service should now look like this.

MockService The mock actions and mock responses windows should look like this. ActionsResponses With these framework elements configured, we now need to equip the mock action to know which response to invoke when called by a particular REST request. This is done by means of a DISPATCH script, accessed as part of the mock action window by clicking the Dispatch (SCRIPT) button. Create a dispatch script with the following code.

1
2
3
4
5
6
7
8
9
10
switch( mockRequest.getPath() ) {
	case '/posts':
		return 'PostsByTitle'

	case '/comments':
		return 'CommentsByName'

	default:
		log.warn 'Unknown resource type encountered'
}

This script uses the value of the request path to select the appropriate response.

Creating the Mock Content

Part of designing a mock is determining the content of the mock’s response. How carefully do you want to design the data that the mock should show? Will the mock only be needed for a couple of days or will it be used for several months?

For this example, we are in the happy situation of having access to the database containing the data that a fully implemented service would return. We can imitate the behaviour of a complete service within the mock with a simple SQL call. In the article relating to database techniques, I discuss the JDBC test step type and its ability to run SQL queries against a database. In the context of a mock, we don’t have access to a specific test step type, but we can quite easily make SQL calls from within a groovy script.

Open the PostsByTitle mock response window and set the Content | Media type list to application/json. Then copy the following code into the script window.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import groovy.sql.* 
import groovy.json.*
final def driver = context.expand( '${h2Driver}' ),
		dbConn = context.expand( '${#Project#dbConnection}' )

def db = null
try{
	db = Sql.newInstance( dbConn, driver )
	final def filter	= mockRequest?.getRequest()?.getQueryString().split( "=" )[ 1 ],
			queryStr	= "select * from posts where title like '" + filter + "%'",
			rows		= db.rows( queryStr )
	mockResponse.setResponseContent( new JsonBuilder( rows ).toPrettyString() )
}
catch( Exception e ) { log.error e.toString() }
db?.close()

This code introduces several techniques that may be unfamiliar, so I’ll take the time to go through them in depth.

Lines 3 - 4

final def driver = context.expand( '${h2Driver}' ),
		dbConn = context.expand( '${#Project#dbConnection}' )

These lines use global and project-based properties to define the strings needed to create a connection to the database. The use of properties allows new database definitions to be used without needing to update the script.

Line 8

	db = Sql.newInstance( dbConn, driver )

This line establishes a connection to the database. As it’s within a try block, it’ll allow any execptions to be caught and handled.

Lines 9 - 10

	final def filter	= mockRequest?.getRequest()?.getQueryString().split( "=" )[ 1 ],
			queryStr	= "select * from posts where title like '" + filter + "%'",

These lines build the SQL query we’re about to submit to the database. The filter variable is derived from the query parameter in the request string e.g. title=qua.

The SQL query eventually submitted to the database via the queryStr will look like this:

1
2
3
SELECT *
FROM posts
WHERE title LIKE 'qua%'

This statement will retrive from the database all rows in the posts table where the values in the title column begin with ‘qua’.

Line 8

	mockResponse.setResponseContent( new JsonBuilder( rows ).toPrettyString() )

The last task of interest is to convert the rows returned from the database into a JSon string we can set as the response content. The script finishes by closing the database connection.

The PostsByTitle window should now look like this. PostsByTitle

Starting the mock service

The last remaining task before we can successfully call the mock service is to ensure that it’s running. Right-click on the Virtualized JSonPlaceholder mock service node in the project tree and select Start Minimized from the context menu. You should see a new window appear minimized in the workspace.

I have found a couple of occasions that the first time I attempt to run a request against the mock service I get the following SQL Exception from my H2 database: No suitable driver found for jdbc:h2:tcp://localhost/~/test:sa;ALIAS_COLUMN_NAME=TRUE. I haven’t tried this against other databases and I haven’t found the solution, but an easy workaround is to open a JDBC step and click the Test Connection button. This seems to be enough to allow the mock response’s script to connect to the database.

You should be now able to open the GetPostsByTitle test step and execute it, seeing the response containing four post records whose title members start with ‘qua’. If you change the content of the title parameter to be ‘quas’ and execute the test step again, you should see only two records returned, as below. CompletedMock At this point I usually add an HTML 200 status assertion to the REST test step, but I don’t attempt to assert on the content. A key concept to bear in mind when developing a virtualised service is that you are not testing the mocks. Don’t spend time on getting them perfect because they will almost certainly change by the time you have a geniune implementation to test against.

Implementing an XML Response

You’ll recall that we created two methods in the service definition, one with a request called request - JSON and the other with a request called request - XML. I’d like to take the opportunity to show how the mocks can return data in XML format as well as in JSON.

To do this, open the CommentsByName mock response and set the Content | Media type list to application/xml. Then copy the following code into the script window.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import groovy.sql.*
final def driver = context.expand( '${h2Driver}' ),
		dbConn = context.expand( '${#Project#dbConnection}' )

def db = null
try{
	db = Sql.newInstance( dbConn, driver )
	final def filter 	= mockRequest?.getRequest()?.getQueryString().split( "=" )[ 1 ],
			queryStr	= "select * from comments where name like '" + filter + "%'",
			rows 	= db.rows( queryStr ),
			writer	= new StringWriter(),
			xml 		= new groovy.xml.MarkupBuilder( writer )
	
	xml.COMMENTS { 
		rows.each { row ->
			xml.COMMENT { row.each { key, value -> "$key" "$value" } }
		}
	}	

	mockResponse.setResponseContent( writer.toString() )
}
catch( Exception e ) { log.error "$e" }
db?.close()

The approach taken in this script is conceptually identical to that used to generate the JSON response. The only difference is the use of a StringWriter and an xml.MarkupBuilder class to convert the rows returned by the database into XML that can appear in the mock response. The CommentsByName mock response window should now look like this. CommentsByName As long as the mock service is still running, you can now execute the GetCommentsByName test step so that the five comment records whose name fields start with ‘id’ are returned, formatted as XML. If you change the value of the name parameter to ‘ex’ and re-run the test step you should get nine records returned, as in this screenshot. CommentsAsXML

The Next Steps

With your mock service in place you might be tempted to spend time developing it further. Before doing this I urge you to review SmartBear’s excellent overview on the subject of mocking, which gives a clear summary of the benefits and costs of this practice. One quote in the overview is worth bearing in mind:

Your First Mock should be available within hours of starting to build it.

The key with mocking is to remember that the mock services are disposable, and that you should be prepared to discard them when the real services become available. The danger with investing too much time into developing your mock is that the eventual implementation will be different, and you will have wasted your effort. The approach described in this article will work if you have a database with test data available and should be re-useable in a range of different testing situations.