Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Web Development

Unit Testing Web Services


Paul is a senior developer with more than 20 years of experience writing software. His book Unit Test Frameworks discusses unit testing and Agile development from a language-independent perspective. He can be contacted at [email protected].


Web services (WS) are becoming a key architectural aspect of large-scale distributed applications. Technology leaders are both launching service-centric applications, such as Google Earth, eBay Web Services, and Amazon's E-Commerce Service, and creating tools that enable other organizations to publish web services. The standards-based, fault-tolerant, self-describing nature of web services enable the creation of loosely coupled applications that run on heterogeneous systems. Web services also enable interoperability between different online applications and enable innovative, synergistic combinations: mashups. While the prime directive for developers once was "put it on the Internet", the main goal now is often to "make it a Service Oriented Architecture (SOA)." For Agile developers, following best practices by doing Test-Driven Development (TDD) of WS code can require extra effort, but is definitely feasible. In this article, I present techniques and patterns for unit testing WS applications to enable TDD as well as general-purpose testing.

Web service applications are inherently multitier. The remote code calling the service methods can be a user's browser-based client or desktop application. It could also be another web service; Tim Berners-Lee and others have stated their expectations that the future Semantic Web will be based on automated agents exchanging information via web services (see "The Semantic Web," www.sciam.com/article.cfm?id=the-semantic-web). Service methods are implemented by the service interface, which contains functionality to generate, send, and receive messages. The service interface is distinct from the service's application logic, and in some cases, is automatically generated based on a service description in Web Services Description Language (WSDL) or a similar schema. So, every WS application has at least three tiers: the service application, the service interface, and the client interface.

In Figure 1, MyService is the service interface layer, MyApp is the service application, and ServiceStub is the client interface that enables remote code to call the web service. MyService implements the WS messaging, while MyApp contains application logic, business rules, data persistence, and other service implementation layers.

[Click image to view at full size]

Figure 1: Top-level components of a typical web service application.

WS messages may contain complex objects serialized as XML, or simple HTTP GET/POST operations. Although it can be tempting to trust that WS messaging just works, generation and communication of such messages represents a significant potential point of failure, and screams for test coverage (at least for followers of the Agile testing mantra, "only the paranoid survive.") Because web services are a presentation layer of sorts, representing the front end of the WS application from the client's perspective, they can be tested using an approach that is analogous to testing GUI code (see Agile User Interface Development, www.onjava.com/pub/a/onjava/2004/11/17/agileuser_1.html), by separating the presentation layer from the application logic. In this case, the presentation layer is the WS messaging functionality. Following this approach, each method and behavior of the web service interface should have corresponding tests. However, to validate how the service methods work "over the wire," it's not sufficient just to create an instance of the service interface class and invoke its methods directly. The service methods must be validated by calling them via the client interface, the same way they are called in the live application. In addition to verifying the end-to-end functionality of the service methods, this also permits testing additional behaviors of the service, such as service resources, client connections, timeouts, and error handling. For these reasons, it is important to test SOA applications by making the tests interact with a live instance of the service, as in Figure 2.

[Click image to view at full size]

Figure 2: Test class MyServiceTest, which tests web service methods by calling an instance of the service running in a service container.

The pseudocode in Listing One is from the test class MyServiceTest, showing how the test method testGetDoc() connects to the service, calls the service method getDoc(), and validates the results. The method setUp() creates the test's service connection, and tearDown() closes it.


public class MyServiceTest extends TestCase {
    public void setUp() {
        String URI = "http://localhost/services/MyService";
        // ... create service endpoint and connect to URI ...
        MyService service = endpoint.getMyService();
    }
    public void testGetDoc() {
        service.putDoc( "test" );
        assertEquals( "test", service.getDoc() );
    }
    public void tearDown() {
        service.destroy();
    }
}

Listing One

When developing web services using TDD, the typical pattern is to first create a test for each new service behavior, then implement the service interface functionality that enables the test to pass. So, as with conventional objects, the organization of the test class tends to parallel that of the production class, with test methods that correspond to each service method or behavior. Figure 3 shows how service methods in MyService are validated by corresponding test methods in MyServiceTest.

[Click image to view at full size]

Figure 3: Service interface MyService and its test class MyServiceTest.

Once the service interface has a test fixture, developing additional tests to validate the service is easy, and developers become accustomed to adding unit tests for every new behavior or bug fix associated with the service, just as for any other object. For example, we found that a service method called getImage() that returned a binary image data array had a serious bug, because if the service treated the data as a string, it could contain XML characters such as "<" and ">" that resulted in malformed SOAP messages. To fix this, we modified the WSDL declaration of getImage() to represent the data as a base64-encoded array, as in Listing Two.


<?xml version="1.0" encoding="UTF-8"?>
<definitions name="MyService"
    targetNamespace="http://services.corp.com/namespaces/MyService">
<!— ... —>
        <!— getImage —>
        <xsd:element name="getImageRequest" type="xsd:string"/>
        <xsd:element name="getImageResponse">
            <xsd:complexType>
               <xsd:sequence>
                  <xsd:element name="data" type="xsd:base64Binary"/>
               </xsd:sequence>
            </xsd:complexType>
        </xsd:element>
<!— ... —>
</definitions>

Listing Two

The test method testGetImage() validates that getImage() succeeds in returning image data that exhibited the malformed message bug prior to the fix (Listing Three). It also validates that the data array has the expected size after being base64-encoded by the service, communicated as a WS message, and decoded by the client interface.


public class MyServiceTest extends TestCase {
    public void testGetImage() {
        GetImageResponse r = service.getImage( "test.image.1" );
        assertEquals( 72000, r.getData().length );
    }
}

Listing Three

In addition to verifying the service methods' results for normal use cases, the tests should also validate error-handling characteristics; for example, are the expected exceptions or error codes generated by the service and caught by the client? The test method testInvalidLogin() validates that the service throws an expected login exception (Listing Four). This is not as trivial a test as it seems at first, since the service exception is also a WS message that is serialized and deserialized at the service and client endpoints.


public class MyServiceTest extends TestCase {
    public void testInvalidLogin() {
        try {
            service.login( "not_a_real_login", "bad_password" );
            fail( "Expected LoginException not thrown" );
        } catch ( LoginException e ) {} // expected exception
    }
}

Listing Four

Service tests can validate behaviors that are difficult to test manually. The test method testTimeout() shows validation of a service connection timeout, a behavior that can be tested manually only by waiting for the normal session timeout to expire, or by altering and redeploying the service configuration to set a short timeout period, either of which are tedious (Listing Five).


public class MyServiceTest extends TestCase {
    public void testTimeout() {
        service.setTimeout( Calendar.SECOND, 1 );
        try {
            Thread.sleep(1010); // wait for timeout
        } catch (InterruptedException e) {}
        try {
            service.putDoc( "test" ); // try to call service
            fail( "Expected RemoteException not thrown" );
        } catch ( RemoteException e ) {} // expected exception
    }
}

Listing Five

Another service behavior that is a pain to test manually is multiple client connections. The test method testMultipleConnections() validates that different clients have different service-side state by creating two client connections to the service, performing an operation on one, and verifying that the second client does not see the data added by the first (Listing Six).


public class MyServiceTest extends TestCase {
    public void testMultipleConnections() {
        MyService service1 = endpoint.getMyService();
        MyService service2 = endpoint.getMyService();
        service1.putDoc( "test" );
        assertFalse( "test" == service2.getDoc() );
    }
}

Listing Six

Creating tests that exercise the web service not only enables unit testing of service methods, but also facilitates higher level testing, such as performance testing. A common performance criterion is to validate that a service method completes in less than a specified amount of time. This would be difficult to validate manually without instrumenting the code and performing user actions that fire service calls. The test method testGetDocTime() shows how to implement such a performance test (Listing Seven).


public class MyServiceTest extends TestCase {
    public void testGetDocTime() {
        long startTime = System.currentTimeMillis();
        String doc = service.getDoc();
        long endTime = System.currentTimeMillis();
        assertTrue( endTime-startTime < 100 );
    }
}

Listing Seven

Similarly, using the test framework to create stress tests of the service is simple. The test method testLargeDoc() validates that an arbitrarily large document can be sent to and retrieved from the service without alteration (Listing Eight).


public class MyServiceTest extends TestCase {
    public void testLargeDoc() {
        StringBuffer doc = new StringBuffer(1000000); // 1 MB string
        for (int i = 0; i<1000000; i++) doc.append("X");
        service.putDoc( doc.toString() );
        String d = service.getDoc();
        assertEquals( doc.toString(), d );
    }
}

Listing Eight

Another useful type of stress test is to create many client connections in separate threads, each asynchronously calling service methods, to validate the scalability of the service and application.

Using such testing techniques, web services can be automatically and repeatedly validated to provide a much higher level of quality than what is possible through ad hoc manual testing. Once the web service interface has full test coverage, the underlying application can be rapidly iterated with confidence that the WS messaging layer will work as expected. Given the increasing focus on and complexity of distributed, service-oriented architectures and applications, web service development and testing practices such as those described here will become increasingly indispensable.

Acknowledgments

This article resulted in part from Grid and web service development work at Tech-X Corporation, including the project "TxFlow: Data Skimming Grid Portal," led by Dr. David Alexander and funded by DOE contract DE-FG02-03ER83799, and the project "GRIDL: Grid-Enabled Interactive Data Language for Astronomical Data," led by Dr. Svetlana Shasharina and funded by NASA contract NNX09CA21P.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.