Test-driven development for server-side applications Unit tests require a granularity that is hard to achieve when testing components inside of a server-side container — which is exactly why some test-driven developers use Jakarta Cactus. Cactus extends the popular JUnit testing framework with an in-container strategy that enables you to execute test cases for servlets, EJBs, and other server-side code. In this Open source Java projects installment, Steven Haines shows you how to write Cactus test cases for a servlet and run them automatically. Software developers today have widely embraced test-driven development (TDD) for the simple reason that tested code works better. Furthermore, if you introduce new code into a working application and it inadvertently breaks something, your test cases will show you what specific functionality is broken and where. For example, suppose your “test sorting with wildcards” test case fails, stating that it expected 10 results from the search string “happy*” but only 8 were returned. You know which specific results are missing, so you know where to start your investigations — all from a test case that you might have written a year ago! TDD for more complex components such as servlets and EJBs (Enterprise JavaBeans), though, can be problematic, because these components expect to run inside a container. Sure, you can build a mock servlet or EJB container and test your code against the specifications. That’s a valid test that might be worth doing. But a container’s idiosyncratic behaviors might cause your application to behave differently in different deployment environments. This is a problem because TDD dictates that tests be run automatically, whereas deploying an application to a container and testing it with the granularity required of a unit test is a cumbersome manual process. Even if you use a solution for automatic deployment (such as Cargo), unit-test-level granularity is still an issue. For example, you can deploy an application to a container, execute some piece of functionality, and then validate the results — but that doesn’t test internal servlet methods or examine internal variables such as the session or servlet context to see if variables are set appropriately. It only allows for external “black-box” tests. Origins of TDD Many consider TDD to be formalized by Kent Beck in his book Test-Driven Development: By Example, through which he addressed the problems with the state of code testing and the resultant poor quality of applications. In traditional development environments, developers spend a considerable amount of time writing code and integrating their components into an application; then they pass that application to quality assurance (QA) for testing. QA then tests the application from a business test-case level, which is more like black-box testing: when I invoke functionality X I expect to see results Y. If the application returns the correct results, then it passes; if not, it fails. The challenge with this level of testing is that the code’s underlying complexity can’t be effectively exercised only from business test cases. For example, a search function might behave differently (follow a different path through the code) depending on the input. The business test cases might not call this out explicitly, but a code optimization might make it preferable. The business test case might not exercise functionality that users eventually will. So one of the tenets of TDD is that developers need to write their own test cases: they understand the different code paths that different options can invoke and are best suited to write accurate test cases. Jakarta Cactus provides a framework for testing servlets and EJBs running inside different containers to obtain unit-test-level metrics. You can determine not only whether your application is returning the correct results, but also whether the application is behaving correctly internally. This article describes how to use Jakarta Cactus to write JUnit-style test cases to test a Web application inside multiple Web containers. You’ll also learn how to automate your tests using Apache Ant, which you can easily extend to be launched by a Continuous Integration server like Hudson. TDD basics TDD is implemented through the following steps, illustrated in Figure 1: Add a new test to the test suiteProve that the test failsImplement the new functionalityProve that the test succeedsRefactor the codeFigure 1. The TDD process (Click to enlarge.) You write a new test is before writing code so that the test harness can prove that the new test case fails; this validates that the test harness is valid. If you write a flawed test case and it succeeds, you might not be detecting problems that the test case is meant to identify. But if you can prove that the test fails without your code and succeeds with it, then you can have confidence in the test itself. Finally, once you have a valid test case, you are free to refactor the code to find a more elegant solution (because if you inadvertently break the code, your test case will find it!). Testing Web applications Developing a test case with Cactus that exercises a servlet involves extending the org.apache.cactus.ServletTestCase class and then implementing various test methods. If you have written test cases with JUnit, you’ll find writing Cactus test cases to be straightforward. The ServletTestCase class is an extension of the JUnit TestCase class, which provides lifecycle methods, such as setUp() and tearDown(), and a host of assertion methods for you to use to test your conditions. Cactus does not yet support JUnit 4 annotations, so you need to define your test cases using older JUnit criteria: The test method must start with test.The test method must be public.The test method must return void. To support testing servlets, the ServletTestCase defines two additional lifecycle-management functions and four implicit objects. Test-case names are identified using the aforementioned criteria, and then those test cases can define these two lifecycle-management methods: beginTestCaseName, called before the test case is executed so that you can define components of the request such as the URI and the machine nameendTestCaseName, called after the test case is executed so that you can validate the results of the servlet execution For example, if you define the following test case: public void testMyTestCase() You could then hook into MyTestCase‘s lifecycle by defining two methods: public void beginMyTestCase(WebRequest theRequest) public void endMyTestCase(WebResponse theResponse) The ServletTestCase defines the following implicit objects to help you when working with Web requests: ServletConfig configHttpServletRequest requestHttpServletResponse responseHttpSession session The config object is used to initialize the servlet and is passed to the servlet’s init() method. The request and response objects are passed to the service() method (or derivative methods such as doGet() or doPost()), which enables you to add request attributes before a servlet is invoked and review the response when the request completes. Finally, if your application uses HTTP sessions, you can access the session object to set up scenarios and to ensure that the correct session parameters are modified in the course of a request. The process that you implement to build a servlet test case is: Create a buildMyTestCase() method in which you set up the request and add cookies.Create a testMyTestCase() method in which you invoke servlet methods and examine session attributes.Create an endMyTestCase() method in which you examine the response and review cookies. So how does Cactus fit into this process? Figure 2 (originally published on the Cactus Web site) shows how. Figure 2. Building a servlet test case (Click to enlarge.) Cactus “cactifies” your WAR file to inject support for Cactus. Reviewing Figure 2: JUnit invokes the local begin() or beginTestCase() method.The test case, using HttpClient, sends the request to the Cactus proxy.The Cactus proxy invokes the testTestCase() method on the test case that is running in the container.The test case invokes the test scenario on the running servlet.The test case returns the results back to the proxy.The proxy returns the results to the local test case and invokes the end() or endTestCase() method.An example To illustrate how to build a Cactus test case I built a servlet that we’ll test. It has the following behavior: It loads a set of messages in its init() method and stores those in the ServletContext.It maintains a cookie named repeatCustomer to note how many times a customer has visited the site. It is created the first time the user accesses the servlet.It stores the user’s name in the session object.In the resultant JSP file, a first-time visitor is presented with different verbiage from a repeat customer.The servlet makes all of this information available in the HttpServletRequest for the JSP file to present it. Listing 1 shows the source code for the TestServlet class. Listing 1. TestServlet.javapackage com.geekcap.cactustest.web; import java.io.IOException; import java.util.HashMap; import java.util.Map; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; public class TestServlet extends HttpServlet { private ServletConfig config; @SuppressWarnings("unchecked") public void init( ServletConfig config) throws ServletException { // Save our config this.config = config; // See if we've already initialized the servlet Map<String,String> myMap = ( Map<String,String> )config.getServletContext().getAttribute( "myMap" ); if( myMap == null ) { // Pretend to load data from a configuration file... myMap = new HashMap<String,String>(); myMap.put( "initialVisit", "Welcome to the site!" ); myMap.put( "repeatCustomer", "Welcome back to the site!" ); // Add our map to the application context config.getServletContext().setAttribute( "myMap", myMap ); } } public void service( HttpServletRequest req, HttpServletResponse res ) throws ServletException { // Greeting string String greetingString = null; // Load our greeting string map Map<String,String> myMap = ( Map<String,String> )config.getServletContext().getAttribute( "myMap" ); System.out.println( "MyMap: " + myMap ); // Load our cookies boolean repeatCustomer = false; Cookie[] cookies = req.getCookies(); if( cookies != null ) { // Loop over all the cookies and look for a repeatCustomer cookie for( int i=0; i<cookies.length; i++ ) { Cookie cookie = cookies[ i ]; System.out.println( "Found cookie: " + cookie.getName() + " = " + cookie.getValue() ); if( cookie.getName().equalsIgnoreCase( "repeatCustomer" ) ) { // This is a repeat visitor so increment his visit count int visits = Integer.parseInt( cookie.getValue() ); String visitsStr = Integer.toString( ++visits ); cookie.setValue( visitsStr ); res.addCookie( cookie ); // Add the number of visits to the reques so that it can be presented // by the JSP page req.setAttribute( "repeatCustomer", visitsStr ); // Mark the user as a repeat customer repeatCustomer = true; // Set the greeting string greetingString = myMap.get( "repeatCustomer" ); } } } if( !repeatCustomer ) { // Setup a new cookie res.addCookie( new Cookie( "repeatCustomer", "1" ) ); // Save for reference in the JSP req.setAttribute( "repeatCustomer", "1" ); greetingString = myMap.get( "initialVisit" ); } // Save the greeting string in the request to be presented in the JSP req.setAttribute( "greetingString", greetingString ); // Load request attributes String customerName = ( String )req.getAttribute( "name" ); // Create/Load a session for this customer HttpSession session = req.getSession(); if( customerName != null ) { // We either have the customer's name for the first time or an update session.setAttribute( "name", customerName ); } // Forward to a JSP for presentation try { req.getRequestDispatcher( "test.jsp" ).forward( req, res ); } catch( IOException e ) { e.printStackTrace(); throw new ServletException( e ); } } } The important aspects of Listing 1 are: After initialization, the ServletContext should have a Map named myMap that contains two Strings — one to present when it is the user’s first visit and one to present when it’s a repeat visit.After the user first accesses the site, he or she should have a cookie named repeatCustomer with a value of 1.If we send the user’s name, it should be stored in the HttpSession with the name key. Listing 2 shows a series of test cases that validate the servlet’s behavior. Listing 2. CactusServletTest.javapackage com.geekcap.cactustest.web; import java.util.Map; import java.util.Vector; import javax.servlet.ServletException; import org.apache.cactus.Cookie; import org.apache.cactus.ServletTestCase; import org.apache.cactus.WebRequest; import org.apache.cactus.WebResponse; public class CactusServletTest extends ServletTestCase { /** * Tests the Servlet's init() method to ensure that it loads its data and sets up the * Servlet Context */ public void testInit() { try { // Create the servlet TestServlet servlet = new TestServlet(); servlet.init( config ); // Validate that the data was loaded appropriately Map<String,String> map = ( Map<String,String> )config.getServletContext().getAttribute( "myMap" ); // Assertions assertNotNull( "After initializing the Servlet, myMap was not set", map ); assertEquals( "myMap should contain 2 elements", 2, map.size() ); assertTrue( "myMap is missing the initialVisit key", map.containsKey( "initialVisit" ) ); assertTrue( "myMap is missing the repeatCustomer key", map.containsKey( "repeatCustomer" ) ); } catch( ServletException e ) { fail( e.getMessage() ); } } /** * Called before testFirstVisit() * @param theRequest Defines information about the request to be executed */ public void beginFirstVisit( WebRequest theRequest ) { theRequest.setURL( "localhost", "/CactusTest", "/CactusTestServlet", null, null ); } /** * Tests the scenario of a user's first visit to the application */ public void testFirstVisit() { try { // Create the servlet TestServlet servlet = new TestServlet(); servlet.init( config ); // Test the first time the user hits the servlet request.setAttribute( "name", "Steve" ); servlet.service( request, response ); // Validate that the session is set assertEquals( "The user's name should be set in the session", "Steve", session.getAttribute( "name" ) ); } catch( ServletException e ) { fail( e.getMessage() ); } } /** * Called after testFirstVisit() * @param theResponse Contains the contents of the generated document */ public void endFirstVisit( WebResponse theResponse ) { // Validate our cookie Cookie cookie = theResponse.getCookie( "repeatCustomer" ); assertNotNull( "The repeatCustomer cookie has not been set", cookie ); assertEquals( "This is the user's first visit, so the repeatCustomer cookie should have a value of 1", "1", cookie.getValue() ); // Validate the response String text = theResponse.getText(); assertTrue( "Missing or incorrect title on the resultant page", text.indexOf( "<title>Cactus Test Page</title>" ) != -1 ); assertEquals( "The response code should be 200", 200, theResponse.getStatusCode() ); } } The first test case is testInit(). This test case does not have beginInit() or endInit() methods, which are optional, and exists only to check the servlet-initialization code. When invoked, it creates an instance of the TestServlet and invokes its init() method using the implicit config object. Afterwards it examines the contents of the config‘s ServletContext to ensure that it contains the map, that the map has two elements, and that it contains the two keys initialVisit and repeatCustomer. Open source licenses Each of the open source Java projects covered in this series is subject to a license, which you should understand before integrating the project with your own projects. Cactus is subject to the Apache License; see Resources to learn more. The purpose of assertions is to validate some condition. For example, in the testInit() method, the assertion validates that the map is not null, that the map’s size (number of elements) is equal to two, and that the map contains the two keys. If the condition fails, the first String in the assertion is presented as the failure message. So if the map is not in the ServletContext, the test will fail with the message: “After initializing the Servlet, myMap was not set.” The JUnit Assert class provides assertion methods for comparing equality of the Java primitive types, true and false for booleans, null and not null for objects, and object equality. For more information about the variations of assertion methods, review the Assert class in the JUnit Javadoc. The second test case — testFirstVisit() — is more substantial and illustrates the power of Cactus. The beginFirstVisit() method initializes the request sent to the proxy so that it can set up the HTTP headers to establish the context for the request. The setURL() method is defined as: void setURL(java.lang.String theServerName, java.lang.String theContextPath, java.lang.String theServletPath, java.lang.String thePathInfo, java.lang.String theQueryString) Sets the simulated URL. A URL is of the form : URL = "http://" + serverName (including port) + requestURI ? queryString Where requestURI = contextPath + servletPath + pathInfo After the beginFirstVisit() method is invoked, the URI is constructed and sent to the Cactus Web proxy, and then the testFirstVisit() is invoked inside the servlet container. The beginFirstVisit() method creates and initializes the TestServlet class, sets the name request parameter, invokes the TestServlet‘s service() method, and then examines the session object to ensure that the name was properly set. At this point we’re free to examine the physical request, response, and session objects. The actual response that is generated by the servlet (and hence the JSP page) will be available in the WebResponse object that is passed to the endFirstVisit() method. After the Cactus Web proxy finishes invoking the test case on the servlet, it returns its results to the Cactus client, which invokes the endFirstVisit() method, passing it a WebResponse object. The WebResponse object contains the text of the response, access to cookies that have been set, and the response code. The endFirstVisit() method first checks to see if the repeatCookie has been set and that it has been initialized to a value of 1. Next it obtains the text of the response and searches for the HTML <title> tag. Finally, it examines the response code to ensure that it is set to 200, which signifies that the request completed successfully. When you write test cases, the general process is to initialize the request information, including cookies, in the beginTestCase() method, set request and session parameters and invoke servlet methods in the testTestCase() method, and then examine the response, including cookies, in the endTestCase() method. Cactus handles all of the work of deploying your application to your containers, invoking the servlet code, and then sending the response back to your test case. Installation and configuration To install and configure Cactus you’ll need to download it and its dependencies: Download Cactus and decompress it to your local computer.Download the following dependencies: Commons CodecJDOMJaxenTomcat (optional, but in the example I’m deploying to Tomcat)Decompress Cactus and these external JAR files and remember where you put them; you’ll need them in the next section.Automate your testing The best way to perform the servlet-testing steps I outlined earlier is through automation. You can do so using the Cactus Ant tasks and a script that executes the sample application. First you need to set up the Ant classpath with the Cactus classes and the external dependencies that you just downloaded: <!-- Libraries required for the Cactus tests --> <property name="cactus.home" location="/home/shaines/lib/cactus-1.8.1-bin" /> <property name="jdom.home" location="/home/shaines/lib/jdom-1.1" /> <property name="jaxen.home" location="/home/shaines/lib/jaxen-1.1.1" /> <property name="aspectjrt.jar" location="${cactus.home}/lib/aspectjrt.jar"/> <property name="cactus.jar" location="${cactus.home}/lib/cactus.jar"/> <property name="cactus.ant.jar" location="${cactus.home}/lib/cactus.ant.jar"/> <property name="commons.httpclient.jar" location="${cactus.home}/lib/commons-httpclient-3.1.jar"/> <property name="commons.logging.jar" location="${cactus.home}/lib/commons-logging-1.1.jar"/> <property name="commons.codec.jar" location="/home/shaines/lib/commons-codec-1.3/commons-codec-1.3.jar"/> <property name="httpunit.jar" location="${cactus.home}/lib/httpunit.jar"/> <property name="junit.jar" location="${cactus.home}/lib/junit.jar"/> <property name="jdom.jar" location="${jdom.home}/build/jdom.jar"/> <property name="xerces.jar" location="${jdom.home}/lib/xerces.jar"/> <property name="jaxen.jar" location="${jaxen.home}/jaxen-1.1.1.jar"/> <property name="nekohtml.jar" location="${cactus.home}/lib/nekohtml.jar"/> <path id="cactus.classpath"> <path refid="classpath.test"/> <pathelement location="${aspectjrt.jar}"/> <pathelement location="${cactus.jar}"/> <pathelement location="${cactus.ant.jar}"/> <pathelement location="${commons.httpclient.jar}"/> <pathelement location="${commons.codec.jar}"/> <pathelement location="${commons.logging.jar}"/> <pathelement location="${junit.jar}"/> <pathelement location="${jdom.jar}"/> <pathelement location="${xerces.jar}"/> <pathelement location="${jaxen.jar}"/> </path> <!-- Build the CLASSPATH --> <path id="classpath"> <fileset dir="${tomcat.home}/lib" includes="servlet-api.jar" /> </path> <path id="classpath.test"> <fileset dir="${tomcat.home}/lib" includes="servlet-api.jar" /> <fileset dir="${junit.home}" includes="junit-4.5.jar" /> <fileset dir="${cactus.home}/lib" includes="*.jar" /> <pathelement location="${build}/classes"/> </path> Next we need to define the Cactus and Cargo tasks: <!-- Import the Cactus Tasks --> <taskdef resource="cactus.tasks" classpathref="cactus.classpath"/> <!-- Import the Cargo Tasks --> <property name="cargo-core-uberjar.jar" location="${cactus.home}/lib/cargo-core-uberjar-1.0-beta-2.jar"/> <property name="cargo-ant.jar" location="${cactus.home}/lib/cargo-ant-1.0-beta-2.jar"/> <taskdef resource="cargo.tasks"> <classpath> <pathelement location="${cargo-core-uberjar.jar}"/> <pathelement location="${cargo-ant.jar}"/> </classpath> </taskdef> The Cactus tasks we’ll use are cactus, which executes the servlet test cases, and cactifywar, which sets up the proxy and puts the correct server test-case code into your WAR file. Cargo is another project upon which Cactus depends. It provides an automation interface to control various application servers, including Tomcat, WebLogic, Jetty, Geronimo, and Oracle Application Server. Cargo provides a set of Ant tasks that can be used to deploy an application to an application server as well as start and stop it. The following snippet shows how to “cactify” a WAR file: <target name="test.prepare" depends="dist"> <!-- Cactify the web-app archive --> <cactifywar srcfile="${dist}/lib/CactusTest.war" destfile="${dist}/lib/CactifiedTest.war"> <classes dir="${build}/classes"/> <lib file="${httpunit.jar}"/> </cactifywar> </target> The dist task creates the CactusTest.war file, The test.prepare task executes the cactifywar task that adds support for the Cactus proxy and pushes the server side of the test case into the resultant WAR file. The following snippet shows the cactustest Ant target, which uses Cactus and Cargo tasks: <target name="cactustest" depends="test.prepare" description="Run the tests on the defined containers"> <!-- Run the tests --> <cactus warfile="${dist}/lib/CactifiedTest.war" fork="yes" errorProperty="test.failed" failureproperty="tests.failed"> <classpath> <path refid="cactus.classpath"/> <pathelement location="${httpunit.jar}"/> <pathelement location="${nekohtml.jar}"/> <pathelement location="${build}/classes"/> </classpath> <containerset > <cargo containerId="tomcat6x" home="/home/shaines/apps/apache-tomcat-6.0.18" output="tomcat-output.log" log="tomcat-cargo.log"> <configuration> <property name="cargo.servlet.port" value="8080"/> <property name="cargo.logging" value="high"/> <deployable type="war" file="${dist}/lib/CactifiedTest.war"/> </configuration> </cargo> <cargo containerId="jetty6x" home="/home/shaines/apps/jetty-6.1.3" output="jetty-output.log" log="jetty-cargo.log"> <configuration> <property name="cargo.servlet.port" value="8080"/> <property name="cargo.logging" value="high"/> <deployable type="war" file="${dist}/lib/CactifiedTest.war"/> </configuration> </cargo> </containerset> <formatter type="brief" usefile="false"/> <formatter type="xml"/> <batchtest todir="${reports.dir}"> <fileset dir="${build}/classes"> <include name="**/*Test.class"/> </fileset> </batchtest> </cactus> <junitreport todir="${reports.dir}"> <fileset dir="target/tomcat6x" includes="TEST-*.xml"/> <report todir="${reports.dir}" format="frames"/> </junitreport> <fail message="Tests failed. Please see test reports" if="test.failed" /> </target> The cactus task extends the junit task, which supports the <batchtest> node to identify which test cases to execute. But the important part of the cactus task is that it accepts a set of <cargo> elements in its <containerset>. I configured two <cargo> elements, one for Tomcat and one for Jetty, telling them where I have them installed and the location of the cactified WAR file to deploy to them. You can define as many <cargo> elements in the <containerset> as you like, and your test cases will be evaluated in each container. For the truly adventurous, Cargo also lets you download and install an application server before deploying an application to it. So if you want an untouched and pure environment, review the relevant documentation on the Cargo Web site. You’ll find a link to the complete source code, including the Ant script, for this example in this article’s Resources. When you launch Ant, passing it the cactustest target, you should see output similar to the following: shaines@joshua:~/workspace/CactusTest$ ant cactustest Buildfile: build.xml init: compile: [javac] Compiling 1 source file to /home/shaines/workspace/CactusTest/build/classes [javac] Note: /home/shaines/workspace/CactusTest/test/com/geekcap/cactustest/web/CactusServletTest.java uses unchecked or unsafe operations. [javac] Note: Recompile with -Xlint:unchecked for details. dist: [war] Building war: /home/shaines/workspace/CactusTest/deploy/lib/CactusTest.war test.prepare: [cactifywar] Analyzing war: /home/shaines/workspace/CactusTest/deploy/lib/CactusTest.war [cactifywar] Building war: /home/shaines/workspace/CactusTest/deploy/lib/CactifiedTest.war cactustest: [cactus] ----------------------------------------------------------------- [cactus] Running tests against Tomcat 6.x @ http://localhost:8080 [cactus] ----------------------------------------------------------------- [cactus] Deploying [/home/shaines/workspace/CactusTest/deploy/lib/CactifiedTest.war] to [/tmp/cargo/conf/webapps]... [cactus] Tomcat 6.x starting... Server [Apache-Coyote/1.1] started [cactus] Testsuite: com.geekcap.cactustest.web.CactusServletTest [cactus] Tests run: 2, Failures: 0, Errors: 0, Time elapsed: 0.968 sec [cactus] [cactus] Tomcat 6.x is stopping... [cactus] Tomcat 6.x started on port [8080] [cactus] ----------------------------------------------------------------- [cactus] Running tests against Jetty 6.x @ http://localhost:8080 [cactus] ----------------------------------------------------------------- [cactus] Deploying [/home/shaines/workspace/CactusTest/deploy/lib/CactifiedTest.war] to [/tmp/cargo/conf/webapps]... [cactus] Jetty 6.x starting... [cactus] Tomcat 6.x is stopped Server [Jetty(6.1.3)] started [cactus] Testsuite: com.geekcap.cactustest.web.CactusServletTest [cactus] Tests run: 2, Failures: 0, Errors: 0, Time elapsed: 0.867 sec [cactus] [cactus] Jetty 6.x is stopping... [cactus] Jetty 6.x started on port [8080] [junitreport] Processing /home/shaines/workspace/CactusTest/reports/TESTS-TestSuites.xml to /tmp/null900006209 [junitreport] Loading stylesheet jar:file:/home/shaines/lib/apache-ant-1.7.1/lib/ant-junit.jar!/org/apache/tools/ant/taskdefs/optional/junit/xsl/junit-frames.xsl [junitreport] Transform time: 451ms [junitreport] Deleting: /tmp/null900006209 BUILD SUCCESSFUL Total time: 13 seconds The test cases will run in both Tomcat and Jetty. If a test fails, then the tests will continue until they are all complete, and the build will be marked as a failure. You can review the results in project-directory/reports/index.html. Figures 3 shows a sample report of a failure. Figure 3. A sample report illustrating a failure (Click to enlarge.) Figure 4’s sample report shows a success. Figure 4. A sample report illustrating a success (Click to enlarge.)In conclusion Unit testing and TDD are widely adopted because it has been proven that tested code works better. But unit testing complex applications like servlets and EJBs that run inside containers can be challenging, because unit testing involves being able to examine the internal state of objects and testing internal methods. In order to gain this level of visibility, the test case needs to run inside the container itself, but test cases are typically run from a script in an automated build script, which do not lend themselves well to running inside an enterprise container. Jakarta Cactus provides a solution to this problem by breaking test cases down into parts that run inside the automated build script and parts that can run in the container, and then places a proxy object between them. It uses the Cargo project to deploy a modified component to a container, start the container, execute the test, and stop the container. As a result, you can unit test server-side components inside the different containers where your application is expected to run. Steven Haines is the founder and CEO of GeekCap, Inc., which provides technical e-learning solutions for software developers. Previously he was the Java EE Domain Expert at Quest Software, defining software used to monitor the performance of various Java EE application servers. He is the author of Pro Java EE 5 Performance Management and Optimization, Java 2 Primer Plus, and Java 2 From Scratch. He is the Java host on InformIT.com and a Java Community Editor on InfoQ.com. Steven has taught Java at the University of California, Irvine and Learning Tree University. Open SourceBuild AutomationWeb DevelopmentApp TestingJavaSoftware Development