In the past I wrote an article about how to test een Asynchronous Request Response BPEL process, with WS-Addressing. This uses SoapUI as a test client, to test an Async BPEL Process and catching the response. Frank suggested to create a SoapUI project that mocks the BPEL Process. And that's a whole other ball-game!
SoapUI does support Mock Services. But those are in fact Synchronous services: upon request they send back a response. They're very flexible in that you can determine and select the responses that are send back in several ways. You can even script the lot using an OnRequest Groovy script.
But in this case we do not want to send back a response. The thing with an Asynchronous Request Response service is that they're actually two complementary Fire & Forget services.
- The actual request service is a fire and forget service implemented by the service provider. It does not respond with a message, but it just starts processing the request.
- Then the service client implements a CallBack fire and forget service. Upon processing the request to the point that a response is build, the Service Provider will call this service with the actual response as a request.
For the Async MockService we create a MockService that catches the request. But we leave the response empty: we do not want to reply with a response immediately. In stead, we use the On Request script to call a Test Case that simulates the proces. The interesting part is to pass the info from the request: the WS-Addressing elements (ReplyTo-Address and MessageId) and message Content. But let's sort that out step-by-step.
By the way I worked this out as a Lab together with my colleague Frank, in both SoapUI and ReadyAPI simultaneously. So it works in both products. In stead of ReadyAPI's 'Virts', I stick with the SoapUI term MockServices. But the principles and code snippets work one-on-one.
Create a SoapUI project with a MockService
First create a SoapUI project. I used the wsdl and xsd that I published here on github.Then create a MockService on the BPELProcessAsyncBinding Request binding:
- Name it for example: BPELProcessAsyncBinding MockService.
- Service Path: /mockBPELProcessAsyncBinding
- Port: 8088
Add the mockservice’s endpoint to the interface binding:
Remove the original endpoint from the interface, since it is a dummy endpoint ('http://localhost:7101/soa-infra/services/default/helloWorldBPELAsync/bpelprocessasync_client_ep').
Now you can test the MockService with an adhoc request.
Create a 'Client' Test case
In the SoapUI Project, create a TestSuite called TestSuite AsyncBPEL and add a TestCase, called AsyncSvcClient:Then clone the Adhoc Test Request to the testcase and call it InvokeAsyncService:
To pick up the response we need to add a MockResponse based on the CallBack binding of the wsdl:
Base it on the CallBack Binding of the wsdl:
Take note of the Port and the Path, if you choose to use something else as 8090 and /HelloWorldCallback that I used for this article.
It is important that this step is started as soon as the request is sent. It takes time to startup the MockResponse listener. So, you need to couple it to the corresponding Soap Request step. To do so, you need to get to the properties of the AsyncReceive MockResponse step and set the start step of the MockResponse step to InvokeAsyncService:
This will ensure that when the InvokeAsyncService step is executed the AsyncReceive mock response is started, so that it can be called as soon as the ServiceProvider wants to send back its response.
Note that the xml request of the AsyncReceive step is empty, as well as the response. The response will stay unused, but the request is to capture the callback message from the service provider, as we will see later on.
Setup the Async Service Provider
The MockService inherently is a synchronous mechanism, so normally used to respond with a response message on request. Since we want to implement an asynchronous request-reply mock service, we won’t respond with a message. So the response message stays empty. How are we going to respond then? We will build a second test case, that will be executed on request from a Groovy Script on the MockService. It will build up a context from the request message and providing that to the running testcase we will provide the test case with the information to invoke the AsyncReceive step of the client test case.Thus we create a new test case, and it will do two things:
- Extract the request properties from the context, they will consist of the following properties:
- WS Addressing ReplyTo Address
- WS Addressing MessageId
- HelloWorld Input message (payload elements)
- Do the Callback based on the provided information.
- Create a new TestSuite, called AsyncBPELSvcProvider and add a TestCase, called AsyncSvcProvider.
- Add a SOAP Request step, named CallBackAsyncSvcClient and based that on the BPELProcessAsyncCallbackBinding:
- As a result value provide ‘Hello’ for now.
- As an endpoint set http://localhost:8090/HelloWorldCallback. We will change that to a property, later , fetched from the context.
- Remove a possible assertion to check on the Soap Response Message (since we won’t get one).
- If you want to test now, you can run the AsyncSvcClient but it will wait on the AsyncReceive step. To have that execute, you should manually run the AsyncSvcProvider test case.
Now we need to have the new TestCase called from the OnRequest script of the MockService.
For that we add a few properties to the MockService, to denote the TestSuite and the containing TestCase that implements our ServiceProvider process.
Then using a basic Groovy script that we will extend later on, we make sure that that test case is ran.
- Add two Custom Properties:
- AsyncTestSuite, with value: AsyncBPELSvcProvider
- AsyncSvcProvTestCase, with value: AsyncSvcProvider
- On the OnRequest script of the Mock Service:
Add the following script:def mockService = context.mockService def method = mockService.name+".Response 1.OnRequest Script" log.info("Start "+method) // def project = mockService.project log.info("Project "+project.name) def asyncTestSuiteName = mockService.getPropertyValue( "AsyncTestSuite") def asyncTestSuite = project.getTestSuiteByName(asyncTestSuiteName) log.info("TestSuite: "+asyncTestSuite.name) def asyncSvcProvTestCaseName = mockService.getPropertyValue( "AsyncSvcProvTestCase") def asyncSvcProvTestCase = asyncTestSuite.getTestCaseByName(asyncSvcProvTestCaseName) log.info("TestCase: "+asyncSvcProvTestCase.name) //Log Request log.info(mockRequest.requestContent) // Set Service Context def svcContext = (com.eviware.soapui.support.types.StringToObjectMap)context //Invoke Async Service Provider TestCase asyncSvcProvTestCase.run(svcContext, false) // End Method log.info("End "+method)
What this does is the following:- Define the mockService and the project objects from the context variable.
- Get the TestSuite and TestCase objects based on the MockService property values of the TestCase to be called.
- Create a serviceContext, to be used to do property transfer later on.
- Run the testCase using the created serviceContext.
- Now you can test this by invoking the AsyncSvcClient test case. You might want to remove the current content of the request of the AsyncReceive .
Transfer Request Context properties to ServiceProvider TestCase
Now we want to at least transfer the helloworld input in the request from the MockService to the service provider testcase, so that it can add it to the response message.In the OnRequest Groovy Script we already created a context. We can simply set additional properties to that context. The values we can extract from the request, by xpath.
- Go to the OnRequest groovy script and extend your existing script to reflect the following:
def mockService = context.mockService def method = mockService.name+".Response 1.OnRequest Script" log.info("Start "+method) // def project = mockService.project log.info("Project "+project.name) def asyncTestSuiteName = mockService.getPropertyValue( "AsyncTestSuite") def asyncTestSuite = project.getTestSuiteByName(asyncTestSuiteName) log.info("TestSuite: "+asyncTestSuite.name) def asyncSvcProvTestCaseName = mockService.getPropertyValue( "AsyncSvcProvTestCase") def asyncSvcProvTestCase = asyncTestSuite.getTestCaseByName(asyncSvcProvTestCaseName) log.info("TestCase: "+asyncSvcProvTestCase.name) //Log Request log.info(mockRequest.requestContent) // // Added lines ==> def groovyUtils = new com.eviware.soapui.support.GroovyUtils(context) // Set Namespaces and query request def holder = groovyUtils.getXmlHolder(mockRequest.getRequestContent()) holder.namespaces["soapenv"] = "http://schemas.xmlsoap.org/soap/envelope/" holder.namespaces["bpel"] = "http://xmlns.oracle.com/ReadyAPIHellloWorldSamples/helloWorldBPELAsync/BPELProcessAsync" holder.namespaces["wsa"] = "http://www.w3.org/2005/08/addressing" def helloInput = holder.getNodeValue("/soapenv:Envelope/soapenv:Body/bpel:process/bpel:input") // Set Service Context def svcContext = (com.eviware.soapui.support.types.StringToObjectMap)context svcContext.helloInput=helloInput // <==Added lines // log.info("helloInput: "+svcContext.helloInput) //Invoke Async Service Provider TestCase asyncSvcProvTestCase.run(svcContext, false) // End Method log.info("End "+method)
This adds the following:- A declaration of the groovyUtils, that is used to get an so called XmlHolder that contains the content of the Request in parsed XML Format.
- Declare namespace references in the holder.
- Query the helloInput using the xpath expression: "/soapenv:Envelope/soapenv:Body/bpel:process/bpel:input” from the request.
- Set this as a helloInput property on the service context.
Call it GetContextProperties, and move it as the first step in the TestCase:
def testCase=testRunner.testCase def testSuite=testCase.testSuite def methodName=testSuite.name+"."+testCase.name+".getContextProperties" log.info("Start MethodName: "+methodName) def helloInput=context.helloInput log.info(methodName+" Received HelloInput: "+helloInput) testCase.setPropertyValue("helloInput",helloInput) log.info("End MethodName: "+methodName)
As you can see in the top right corner of the editor, you can see that besides a log variable also a context variable is provided:
This variable will contain the properties we set in the call to the testcase from the MockService.
As you can see we get the property from the context, and set it as a TestCase property.
Configure WS-Addressing
In the previously mentioned blog article you can read how to create a test case that supports WS Addressing to call and test an asynchronous (BPEL) request response service. Now with the above, we have the plumbing in place to add the WS Addressing specifics to simulate and test the Asynchronous RequestResponse Service Provider.We need then to provide and process the following:
- A WS Addressing Reply To Address, based on property values that matches the port and path of the AsyncReceive step.
- A message id that is used to validate if the response back is using the correct provided messageId header value. In a real life case this message Id is used by the SOA Suite infrastructure to correlate the response to the correct process instance that requested it. This is not supported/implemented in SoapUI, since that tool is not meant for that. But we can add an assertion to check the correct responding of this property.
- On the AsyncSvcClient test case add the following properties:
- callbackURI, with value: HelloWorldCallback
- callbackPort, with value: 8090
- callbackHost, with value: localhost
- wsAddressingReplyToEndpoint, with value: http://${#TestCase#callbackHost}:${#TestCase#callbackPort}/${#TestCase#callbackURI}
- wsAddressingMessageId, with no value
- Add a Groovy TestStep to AsyncSvcClient test case, call it GenerateWSAMessageId, and move it to the top, and add the following code:
def testCase=testRunner.testCase def testSuite=testCase.testSuite def methodName=testSuite.name+"."+testCase.name+".GenerateWSAMessageId" log.info("Start "+methodName) def wsAddressingMessageId=Math.round((Math.random()*10000000000)) testCase.setPropertyValue("wsAddressingMessageId", wsAddressingMessageId.toString()) log.info("End "+methodName)
This will do a randomize and multiply it with a big number to create an integer value. - Now we will add the WS Addressing properties to the request. Open the InvokeAsyncService test step and click on the WS-A tab at the bottom:
Set the following properties:- Check Enable WS-A Addressing
- Set Must understand to TRUE
- Leave WS-A Version to 200508
- Check Add default wsa:Action
- Set Reply to to: ${#TestCase#wsAddressingReplyToEndpoint}
- Uncheck Generate MessageID
- Set MessageID to: ${#TestCase#wsAddressingMessageId}
- If you would test this, then the request that will be send will look like:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:bpel="http://xmlns.oracle.com/ReadyAPIHellloWorldSamples/helloWorldBPELAsync/BPELProcessAsync"> <soapenv:Header xmlns:wsa="http://www.w3.org/2005/08/addressing"> <wsa:Action soapenv:mustUnderstand="1">process</wsa:Action> <wsa:ReplyTo soapenv:mustUnderstand="1"> <wsa:Address>http://localhost:8090/HelloWorldCallback</wsa:Address> </wsa:ReplyTo> <wsa:MessageID soapenv:mustUnderstand="1">9094853750</wsa:MessageID> </soapenv:Header> <soapenv:Body> <bpel:process> <bpel:input>Roberto</bpel:input> </bpel:process> </soapenv:Body> </soapenv:Envelope>
You see that the ReplyTo Address is set (as a nested element) and the MessageId. You won’t see this in the Request XML panel, but in the http log or in the script-log since we log the request in the OnRequest script of the MockService. The WS-Addressing properties are added to the soap:header on invoke. - Since we have these elements in the request, we can extract those the same way as we did with the helloInput in the OnRequest script of the MockService. Add the lines denoted with // Added lines ==> and // <==Added lines:
from the following script in your script (or copy&paste complete script):
def mockService = context.mockService def method = mockService.name+".Response 1.OnRequest Script" log.info("Start "+method) // def project = mockService.project log.info("Project "+project.name) def asyncTestSuiteName = mockService.getPropertyValue( "AsyncTestSuite") def asyncTestSuite = project.getTestSuiteByName(asyncTestSuiteName) log.info("TestSuite: "+asyncTestSuite.name) def asyncSvcProvTestCaseName = mockService.getPropertyValue( "AsyncSvcProvTestCase") def asyncSvcProvTestCase = asyncTestSuite.getTestCaseByName(asyncSvcProvTestCaseName) log.info("TestCase: "+asyncSvcProvTestCase.name) //Log Request log.info(mockRequest.requestContent) // def groovyUtils = new com.eviware.soapui.support.GroovyUtils(context) // Set Namespaces and query request def holder = groovyUtils.getXmlHolder(mockRequest.getRequestContent()) holder.namespaces["soapenv"] = "http://schemas.xmlsoap.org/soap/envelope/" holder.namespaces["bpel"] = "http://xmlns.oracle.com/ReadyAPIHellloWorldSamples/helloWorldBPELAsync/BPELProcessAsync" holder.namespaces["wsa"] = "http://www.w3.org/2005/08/addressing" def helloInput = holder.getNodeValue("/soapenv:Envelope/soapenv:Body/bpel:process/bpel:input") // // Added lines ==> def wsaReplyToAddress = holder.getNodeValue("/soapenv:Envelope/soapenv:Header/wsa:ReplyTo/wsa:Address") def wsaInReplyToMsgId = holder.getNodeValue("/soapenv:Envelope/soapenv:Header/wsa:MessageID") // <Added lines // // Set Service Context def svcContext = (com.eviware.soapui.support.types.StringToObjectMap)context svcContext.helloInput=helloInput // // Added lines ==> svcContext.wsaReplyToAddress=wsaReplyToAddress svcContext.wsaInReplyToMsgId=wsaInReplyToMsgId // <Added lines // log.info("helloInput: "+svcContext.helloInput) // // Added lines ==> log.info("wsaReplyToAddress: "+svcContext.wsaReplyToAddress) log.info("wsaInReplyToMsgId: "+svcContext.wsaInReplyToMsgId) // <Added lines // //Invoke Async Service Provider TestCase asyncSvcProvTestCase.run(svcContext, false) // End Method log.info("End "+method)
- These context properties need to be extracted in the GetContextProperties of the AsyncSvcProvider test case, to set those as TestCase Properties. So, add the following properties (with no values) to the AsyncSvcProvider test case:
- wsaReplyToAddress
- wsaInReplyToMsgId
- In the GetContextProperties test step, add the lines with the added properties (or copy and paste the complete script):
def testCase=testRunner.testCase def testSuite=testCase.testSuite def methodName=testSuite.name+"."+testCase.name+".getContextProperties" log.info("Start MethodName: "+methodName) def wsaReplyToAddress=context.wsaReplyToAddress def wsaInReplyToMsgId=context.wsaInReplyToMsgId def helloInput=context.helloInput log.info(methodName+" Received wsaReplyToAddress: "+wsaReplyToAddress) log.info(methodName+" Received wsaInReplyToMsgId: "+wsaInReplyToMsgId) log.info(methodName+" Received HelloInput: "+helloInput) testCase.setPropertyValue("wsaReplyToAddress",wsaReplyToAddress) testCase.setPropertyValue("wsaInReplyToMsgId",wsaInReplyToMsgId.toString()) testCase.setPropertyValue("helloInput",helloInput) // End log.info("End MethodName: "+methodName)
(Since the wsaInReplyToMsgId is an integer, it should be "toStringed"...) - As a pre-final step is to adapt the CallBackAsyncSvcClient step to use the wsaReplyToAddress as an endpoint and the wsaInReplyToMsgId as a header property.
Edit the endpoint in the step to ${#TestCase#wsaReplyToAddress}:
Edit the soap header to:<soapenv:Header xmlns:wsa="http://www.w3.org/2005/08/addressing"> <wsa:MessageID>${#TestCase#wsaInReplyToMsgId}</wsa:MessageID> </soapenv:Header>
- The final step is to add an XPath Match assertion on the AsyncReceive to validate the response of the wsaInReplyToMsgId. Call it WSAInReplyToMessageId and provide the following xpath:
declare namespace wsa='http://www.w3.org/2005/08/addressing'; declare namespace bpel='http://xmlns.oracle.com/ReadyAPIHellloWorldSamples/helloWorldBPELAsync/BPELProcessAsync'; declare namespace soapenv='http://schemas.xmlsoap.org/soap/envelope/'; /soapenv:Envelope/soapenv:Header/wsa:MessageID
As an Expected Result value provide: ${#TestCase#wsAddressingMessageId}. - Test the completed AsyncSvcClient.
You see that the wsAddressingReplyToEndpoint is dynamically build up from the previous properties. The callbackURIand the callbackPort should exactly match the values of the path and the port of the AsyncReceive step (without the initial slash):
The property wsAddressingMessageId does not need a value: we will generate a value in another Groovy TestStep.
1 comment :
Thank you for providing this example. It was very helpful in setting up a SoapUI project where I have an external system send me a SOAP request and my mock service triggers a test step to send an async SOAP request back to the external system in correlation with the original request.
Everything "works", however, for some reason the SOAP request sent from the test step seems to keep the connection open after sending the soap payload. Eventually the connection is closed once the configured timeout (15seconds) on the SOAP request is reached.
The destination service receives the payload and processes it once the connection is closed, but a timeout exception is raised in the log files. This exception seems to block a groovy assertion I have so I'd like to prevent it. Any ideas?
For what it's worth I've deployed this as a .war that I'm running in a Tomcat container. Toggling the SoapUI HTTP setting preference "Close connections after request" had no impact.
12:42:27,486 DEBUG [SoapUIMultiThreadedHttpConnectionManager$SoapUIDefaultClientConnection] Sending request: POST /WSInboundAdapter/services/ResponseService?wsdl HTTP/1.1
12:42:42,503 DEBUG [SoapUIMultiThreadedHttpConnectionManager$SoapUIDefaultClientConnection] Connection closed
12:42:42,503 DEBUG [HttpClientSupport$SoapUIHttpClient] Closing the connection.
12:42:42,503 DEBUG [SoapUIMultiThreadedHttpConnectionManager$SoapUIDefaultClientConnection] Connection closed
12:42:42,503 DEBUG [SoapUIMultiThreadedHttpConnectionManager$SoapUIDefaultClientConnection] Connection shut down
12:42:42,510 ERROR [WsdlSubmit] Exception in request: java.net.SocketTimeoutException: Read timed out
12:42:42,511 ERROR [SoapUI] An error occurred [Read timed out], see error log for details
java.net.SocketTimeoutException: Read timed out
Thanks
Post a Comment