When to use?
Your test case class should extend
JspTestCase
whenever you are unit testing:
- Custom tags,
- Any java code that uses JSP API objects (
PageContext
, ...)This tutorial focuses on testing custom tags, as they are the principal code which uses the JSP API objects. Future versions of this tutorial will expand upon testing actual JSPs.
Overview of Tag Library Testing
Custom tags consist of entries in a Tag Library Descriptor file (TLD) and a tag handler class. Cactus provides the facility to test both aspects of a custom tag. However, since the TLD contains no logic, you will use Cactus primarily to test the tag handler class.
To test the tag handler class, use the implicit objects provided by JspTestCase to set up the initial state for the test. Then create and initialize your custom tag using the
pageContext
implicit object. After setting up the tag, call the lifecycle methods implemented by your tag in the correct order and verify that the methods return the expected results. The tag's output can be inspected in theendXXX()
method.For an additional degree of integration testing, you can create a JSP that exercises your custom tag and call it from within a regular Cactus test case. See the section on Further Integration Testing for details.
To simplify the complexities of managing a tag handlers lifecycle, Cactus provides the helper classJspTagLifecycle
. It is basically a stub implementation of a JSP engines tag management routines, and provides many convenient shortcut methods to test tag handlers. For detailled documentation, check out the corresponding API documentation. Currently,JspTagLifecycle
is only available for JSP 1.2.
Provided Implicit Objects
Cactus automatically initializes the following implicit objects. They are made available to your
setUp()
,testXXX()
andtearDown()
methods as instance variables of theJspTestCase
class (and thus as instance variables of your test case class).See the How it works guide for details on how Cactus initializes these objects.The provided implicit objects are:
request
See
ServletTestCase
request
implicit object for documentation.response
See
ServletTestCase
response
implicit object for documentation.config
See
ServletTestCase
config
implicit object for documentation.session
See
ServletTestCase
session
implicit object for documentation.out
Instance variable name out
Class name public javax.servlet.jsp.JspWriter
Cactus does not wrap the out object.You can use this object to write data to the response, thereby simulating the body of a tag (if the tag does not modify its body). If the tag does modify its body, then you will need to generate a BodyContent object before writing out the simualted body. See
bodyContent
for details.pageContext
Instance variable name pageContext
Class name org.apache.cactus.server.PageContextWrapper
, which inherits fromjavax.servlet.jsp.PageContext
Custom tags rely exclusively on the
pageContext
object to provide information about the enclosing JSP. Therefore this is the most important implicit object for testing custom tags. Cactus provides a very thin wrapper that ensures that all of the objects thatpageContext
returns (such as theServletRequest
frompageContext.getRequest()
) are the correctly wrapped versions available in the other implicit variables.See the javadoc for
org.apache.cactus.server.PageContextWrapper
for more details. You should also look at the samples provided in the Cactus distribution.bodyContent
Instance variable name bodyContent
Class name javax.servlet.jsp.tagext.BodyContent
JspTestCase does not actually provide a
bodyContent
implicit object for use withBodyTags
. However, obtaining one is so easy that it deserves mention here. CallingpageContext.pushBody()
returns an object of typejavax.servlet.jsp.tagext.BodyContent
(which inherits fromJspWriter
). This call also changes the value of the "out" variable stored in page scope (and thus the value ofpageContext.getOut()
). To put test content into thebodyContent
object, simply use its writer methods. To quote Sun's API reference on the matter: "Note that the content of BodyContent is the result of evaluation, so it will not contain actions and the like, but the result of their invocation." See Body Tags for more information.It's important to balance calls topushBody()
with calls topopBody()
--otherwise many servlet engines will not output the tag's body. The easiest way to accomplish this is to call pushBody insetUp()
andpopBody()
intearDown()
.
Custom Tag Set Up
Creating the test fixture for a custom tag test involves several steps. The exact order of the steps can vary depending on the needs of the test. For instance, placing the test data in the correct scope would probably happen before a real JSP began its execution. You can emulate this, or choose to do it after the tag has been in initialized (as described below). In most cases you can determine the exact order of the steps based on what is most convenient for a given test (some steps may be specific to only one test in the
TestCase
and so should be executed after commonsetUp()
code).Step 1: Create the Tag (Required)
Instantiate a copy of the tag you wish to test.
SomeTag tag = new SomeTag();Step 2: Set the pageContext (Optional)
Call the
setPageContext()
method with the implicit object provided by Cactus to register the pageContext with the tag.tag.setPageContext(pageContext);Step 3: Set the tag's attributes (Optional)
If your tag takes attributes, call setter methods to initialize the tag's state. Setters on the tag handler class represent the attributes of custom tags. Thus to emulate this JSP fragment:
<someTag foo="10" bar="11"/>You would need to use the following:
someTag.setFoo("10"); someTag.setBar("11");Step 4: Set the parent tag (Optional)
If you would like the tag you are testing to access a parent tag, you will need to call
tag.setParent(enclosingTag);This will allow tag to successfully call
getParent
andTagSupport.findAncestorWithClass()
. Of courseenclosingTag
will have to be instantiated and set up as well, including another call tosetParent()
if you would like to simulate multiple levels of nesting.Step 5: Create the BodyContent Object (Optional)
If your tag processes its body, call
pageContext.pushBody()
to obtain aBodyContent
. If you employ this step, be sure to also include a call topageContext.popBody()
after the tag finishes execution. See the Body Tags section for more details.Step 6: Set up page state (Optional)
Set up any necessary page state by putting test objects into the appropriate scopes. Tags frequently access data in the session, the request, or the page. If your tag operates on data contained in any of these (or in the application scope), be sure to set up this part of the test fixture. Objects can be placed in these scopes by using the implicit objects provided by Cactus directly, or by accessing them indirectly through the
pageContext
object.request.setAttribute("key", new DomainObject("testValue")); //or pageContext.setAttribute("key", new DomainObject("testValue"), PageContext.REQUEST_SCOPE);
Running the Test
Once the tag has been set up and any necessary page data has been placed in the appropriate scopes, testing a custom tag consists of calling the relevant life-cycle methods and then using JUnit asserts to verify the outcome.
Verifying individual methods
Most of the life cycle methods return
ints
, which signal that the container should take a certain action after the method. For instance, the constantEVAL_BODY_INCLUDE
returned fromdoStartTag()
tells the container to include the tag's body in the JSP's output response. So a tag which conditionally includes its body based on the value of one of its attributes might be verified like this:tag.setValueThatResultsInInclude("correct value"); assertEquals(Tag.EVAL_BODY_INCLUDE, tag.doStartTag());Checking effects on page data
In addition to "listening" for the signals that your tag sends to the container, you may want to verify that the tag's execution has the appropriate effects upon the page data. Use
JspTestCase's
implicit objects to verify that the tag has correctly modified the information. The following snippet verifies that theCatalogListTag
has placed a collection of objects in the request under the key "catalogs":catalogListTag.doStartTag(); Collection catalogs = (Collection)request.getAttribute("catalogs"); assertNotNull(catalogs);Verifying tag output
Use the
endXXX
method to verify that your tag's methods have resulted in the correct data being written to the response.This example uses theendXXX()
signature from Cactus 1.2 or above.public void endSomeTagTest (WebResponse response) { String output = response.getText(); assertEquals("<b>expected output</b>", output); }
Special Cases
There are a few scenarios in custom tag testing that deserve extra attention.
Iteration Tags
To test a tag that repeats its body processing a number of times, simply create a
do-while
loop that mimics the life cycle of an iteration tag://[...tag set up and early life cycle methods omitted...] int count = 0; do { count++; } while (tag.doAfterBody() == tag.EVAL_BODY_AGAIN); tag.doEndTag(); //based on setUp we expect 9 repetitions assertEquals(9, count);You can use a count variable (such as the one illustrated in the example) to check whether the tag's body was processed the expected number of times.
Body Tags
Unless specified otherwise by the deployment descriptor, all tags can include a body, which can in turn include other tags or scriptlet expressions. These are automatically evaluated at run time, and the content of the body is simply written out if the tag signals it should be (with
EVAL_BODY_INCLUDE
for instance). Nothing special is required to test this sort of tag, since the tag is unconcerned about its contents.Testing BodyTags--tags which actually perform some processing on their content--is a little trickier. BodyTags can choose to return a constant (
EVAL_BODY_TAG
in JSP 1.1,EVAL_BODY_BUFFERED
in 1.2) fromdoStartTag()
which signals to the container that the tag would like a chance to handle its own body. If it receives this result, the container callspageContext.pushBody()
to obtain aBodyContent
object. TheBodyContent
object is passed to the tag through the tag'ssetBodyContent()
method. The container then uses this object (the old out object is saved) to capture all of the response writing that goes on in the body of the tag. After the tag's body has been evaluated, the tag itself has a chance to do something with the result of the evaluation in it'sdoAfterBody()
method. After the tag has completed its execution, the container restores the old out object with a call topageContext.popBody()
.To test body tags, your test must replicate this somewhat complicated lifecycle. The following code covers all of the steps as they might appear in a typical test:
//standard set up YourTag tag = new YourTag(); tag.setPageContext(this.pageContext); tag.doStartTag(); //obtain the bodyContent object--presumably doStartTag has returned //EVAL_BODY_TAG or EVAL_BODY_BUFFERED. BodyContent bodyContent = this.pageContext.pushBody(); this.tag.setBodyContent(bodyContent); this.tag.doInitBody(); //write some "output" into the bodyContent so that endXXX can test for it. bodyContent.println("Some content"); bodyContent.print("Some evaluated content " + (5 + 9)); //actually handles the processing of the body tag.doAfterBody(); //after the body processing completes tag.doEndTag(); //finally call popBody this.pageContext.popBody();This sample does not fully replicate the container's handling of the tag (for instance, the tag would only receive the
bodyContent
object if the result ofdoStartTag
indicated that it should do so). However, in a test environment, you can make assumptions if doing so simplifies the workings of the test.Again, you can check that the body of the tag was handled correctly by verifying the total output in theendXXX()
method.TagExtraInfo classes
Cactus does not offer any specific services to support the testing of
TagExtraInfo
classes because they do not depend on any of the implicit objects.
Further Integration Testing
You can use Cactus to test how your tag will react when put into a real JSP. This allows you to verify that there are no problems with the deployment descriptor, or unexpected behavior on the part of the container. You accomplish this by writing a small JSP that makes use of your custom tag, and then calling it from within a Cactus test case. You can even use JUnit assertions within scriptlets to verify certain aspects of the Tag's behavior. However, this method requires that you write a separate JSP for each test case (or lump several cases into a single JSP). Both options pose problems, so it may be best to include one or two tests of this type and rely on the more traditional methods described earlier to ensure total coverage.
The test JSP
All the JSP needs to do is include the tag library that describes the tag you are testing and makes use of it in some way. You can import
junit.framework.Assert
to do some simple checks on the effects of the tag. Here is a short example of a JSP that exercises a tag:<%@page import="junit.framework.Assert"%> <%@taglib uri="WEB-INF/yourTagLib.tld" prefix="example"%> Here is the custom tag this page verifies: <example:someTag variableName="foo" variableValue="bar"/> Here is the JUnit assert that checks whether the tag correctly created a scripting variable named <code>foo</code> with the value "bar": <% //attempt to reference foo will cause a translation error if the tag did not //create the scripting variable Assert.assertEquals("bar", foo); %>It's a bad idea to put too many assertions into the JSP. In the example above, the creation of a scripting variable can only be tested within the JSP page. (The same goes for any objects in page scope, because each JSP creates its own.) If you want to use other assertions with this type of test, call them in your test case after
pageContext.include()
(See below for an example.)The TestCase
To use the test JSP, include it from within a
JspTestCase.
The convenience functionpageContext.include()
takes care of this nicely:public void testSomeTag () throws Exception { pageContext.include("/test_some_tag.jsp"); //an assert to check whether the page also mapped foo into the session assert("bar", session.getAttribute("foo")); }Exceptions that result from either page translation (such as required attributes being omitted, or the tag missing a part of its descriptor entry) or page execution (such as the tag being unable to find required data in the appropriate scope) are automatically be thrown up to this level. If you do not catch them there they will be logged by Cactus/JUnit as failures--which is just what you want.
Any output that the test JSP generates can be checked normally in theendXXX
method.Of course, using this strategy means that you need to put the
test_some_tag.jsp
in the specified location within your web application. If you are using JSP test case your build script should already deploy the redirector JSP, so it should be easy to include another JSP in the build process.