-
Notifications
You must be signed in to change notification settings - Fork 4
Quick Start
Test cases should be put in the src/test/java folder. They can be coded in java or Groovy.
Test classes should have the spring Context configuration annotation and extend the AbstractTestNGSpringContextTests class as shown below:
@ContextConfiguration( locations=("classpath:ws-int-test-beans.xml") ) class WSFuncTests extends AbstractTestNGSpringContextTests
ws-int-test-beans.xml is the spring beans file located at src/main/resources folder.
Test methods should return void and be annotated with @Test (org.testng.annotations.Test). Also, it is a best practice to provide a groups parameter to the annotation such that you can run targeted tests. For e.g "long-running", "test-interface-1". Note that the groups can be comma separated values and same test can belong to multiple groups.
@Test(groups="schema-test") public void sendHolidayRequest()
A callback that should be executed on receipt of an event. Mostly, when the spring-ws embedded http server receives a SOAP message it will send it to one of the configured spring-ws endpoints The Endpoint will in turn create this event which will result in the execution of the callback. Callbacks have to implement the org.menacheri.invmmsg.IMessageCallback interface. An abstract implementation named MessageCallback already exists for this interface and test cases only need to use an anonymous inner class implementation like shown below.
MessageCallback callback = new MessageCallback(){ @Override public void onCallback(IMessage message){ // Do test logic here. } }
At any time any number of test cases may be executing concurrently. A number of messages will be sent to your system and it could in turn send out an equal number of calls to other third party web services and so on. But how do we co-relate what callback needs to be executed for what message? MessageIdentity comes to the rescue here. By subscribing to an in-vm channel(explained next) using a message identity, the test case can make sure that it will execute the right callback for the right incoming xml. The identity itself just uses equals method on the object for comparison. An identity for example, could be a simple String transaction id.
This test tool uses Jetlang in-vm messaging architecture under the hood. Consider the channel to be like a JMS queue, where clients can publish messages and subscribers can listen for them. The idea is the same, however Jetlang is much lighter and far faster than JMS. The class org.menacher.invmmsg.InVMChannel is used as the conduit between spring-ws Endpoint's and test cases. The 3 important messages being
publish(message), subscribe(callback,identity) and subscribeSync(callback, identity)
Take the following scenario. You are developing a web service/system that allows clients to invoke a web service on it. The client is expecting a synchronous reply. However to reply back to client, your system in turn relies on another web service. To test out this scenario, the test suite needs to act as the client as well as the server. While mocking is an option, another way would be as shown below:
- Use the Spring-ws WebServiceTemplate and invoke your web service1 and thereby prompt it to invoke web service2.
- Act as web service2 by defining a Spring-ws web service Endpoint(take a look at src/test/org.menacheri.soapsim.HumanResourceEndpoint) class. This end point will receive the SOAP call from your system and publish it on an in-vm channel.
- A MessageCallback defined earlier by the test case will pickup this message from in-vm channel and provide a reply back to your web service1.
- At client side, check the reply from your web service1 and do necessary assertions.
Step 1
Groovy can be used to create simple xml templates using """ for multiline strings. In order to send a simple xml as String to the remote your service use the following method.
//client is of class org.menacheri.soapsim.WebServiceClient which is thin wrapper over the spring-ws WebServiceTemplate String reply = client.getStringReqReply(xmlRequest,service1Url);
Step 2
The code below shows how to define a spring-ws endpoint method. The class itself will be annotated with @Endpoint spring-ws annotation. Your service will be calling such an endpoint which will create a org.menacheri.invmmsg.IMessage instance using a MessageFactory(Take a look at org.menacheri.soapsim.MessageFactory) or any such means. While parsing the incoming xml it is also necessary to create a IMessageIdentity object and set it on the Message. .
@PayloadRoot(namespace=NAMESPACE_URI, localPart="HolidayRequest") @ResponsePayload public Element processHolidayRequest(@RequestPayload Element holidayRequest) { System.out.println("In processHolidayRequest, thread: " + Thread.currentThread().getName()); Element response = publish(holidayRequest); return response; } public Element publish(Element holidayRequest) { if(null == holidayRequest) return null; Element response = null; IMessage message = messageFactory.createHolidayMessage(holidayRequest); IMessage reply = inVMChannel.publish(message);// Callback gets it now. if(null!=reply){ response = (Element)reply.getPayload();// Reply goes back to client from the callback. } return response; }
Step 3
Define a MessageCallback to be executed when the message is received from your system. i.e. act as web service2.
public IMessageCallback subscribeSyncToHolidayRequest(String status, String empId) { MessageIdentity identity = new MessageIdentityImpl(empId); MessageCallback callback = new MessageCallback(){ @Override public void onCallback(IMessage message){// Message is an in-out parameter println "Callback thread: ${Thread.currentThread().name}" // Get the xml response from the template and set it as pay load on the message. Element response = XMLUtils.convertStringToJdomElement(xmlResponse); // Set the response as the new message pay load. // This will be used by the end point to reply back to client message.setPayload(response); } } // Subscribe the code on the channel. Note the "sync" inVMChannel.subscribeSync(callback,identity); return callback; }
A number of things are going on here. The first being that we define an identity object, this object is what is used to lookup a callback and execute. The incoming xml at the Spring-ws Endpoint will be parsed to get another identity object, if these identity objects match, only then will the callback be executed. The key point being that there is only 1 Channel in play, but multiple subscriptions and callbacks can be added to it.
Step 4
Note the "reply" string in Step 1. This is actually the reply we got like this. Test case -> soap client -> web service1 -> spring-ws end point -> callback (setup by test case before client call) -> web service 1 -> client -> Test case
Time for some assertions!
assertNotNull(reply); def response = new XmlSlurper().parseText(reply).declareNamespace(ws:schemaNamespace); def status = response.Status.text(); assertEquals(status,"SUCCESS");
Execution
To execute the test use Maven.
eclipse -> right click on pom.xml -> run as -> maven test.
Asynchronous integration test cases are mostly not request reply. It would generally include a "flow" of messages. Client sends message 1 to your system, based on some logic your system sends back message 2 to client asynchronously. In the mean time it may send message 3 to another web service.This Web Service2 in turn may send back message 4 to your system. If this scenario sounds familiar to you, then your test cases need to use async subscriptions on the in-vm channel, some waiting logic and so on. Example is shown below.
Step 1
Before test case sends a message to some system from which it expects reply, it should first do a subscription for the expected reply and only then send the message. This will prevent any race conditions.
// Do the subscription first and only then send the message to server in order to prevent race conditions MessageCallback reqCallback = subscribeToHolidayRequest("SUCCESS",template); // Async subscription.
An example async subscription method.
/** * Method does asynchronous subscription and returns a callback on which the test case can * wait on to see if it got executed. * @param status * @param empId * @return */ public IMessageCallback subscribeToHolidayRequest(String status, def template) { MessageIdentity identity = new MessageIdentityImpl(template.empId); MessageCallback callback = new MessageCallback(){ @Override public void onCallback(IMessage message){ println "Async Request Callback thread: ${Thread.currentThread().name}"; setValuesOnTemplateFromRequest(message.getPayload(),template); } } // Subscribe the code on the channel. This is async subscription, // meaning code will be run on a different thread relative to the // message publishing thread. inVMChannel.subscribe(callback,identity); return callback; }
Step 2
Now that subscription is done, the message should be sent to the remote web service/system and then we await on the callback. The await is because the subscription is async and happens on another thread.
MessageCallback reqCallback = subscribeToHolidayRequest("SUCCESS",template); // Async subscription. client.getStringReqReply(holidayRequest,holidayUrl); // Since response is sent async the reply is of no use here. if(!reqCallback.await(10, TimeUnit.SECONDS)){ reqCallback.done();// Required since the callback was never executed. fail ("Message was not recieved in stipulated time. #fail") } // At this point the request callback has executed. Subscribe now for some other request.