<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/css" href="/stylesheets/rss.css"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/">
  <channel>
    <title>Object Mentor Blog: Testing GUIs Part II: JSP</title>
    <link>http://blog.objectmentor.com/articles/2007/02/11/testing-guis-part-ii-jsp</link>
    <language>en-us</language>
    <ttl>40</ttl>
    <description></description>
    <item>
      <title>Testing GUIs Part II: JSP</title>
      <description>&lt;p&gt;How do you test JSPs &lt;em&gt;outside&lt;/em&gt; the container, with no web server running&amp;#8230;&lt;/p&gt;


	&lt;h1 style="text-align:center;"&gt;Testing &lt;span class="caps"&gt;JSP&lt;/span&gt; pages.&lt;/h1&gt;


	&lt;p&gt;One of the more annoying aspects of working on a web application is that you have to deploy it in order to test it.  This doesn&amp;#8217;t apply to everything of course; if you are careful with your design you can test the business rules in plain old java objects.  You can test database access, interface layers, and stored procedures without the web server running.  But testing the &lt;span class="caps"&gt;GUI&lt;/span&gt; &amp;#8211; the &lt;span class="caps"&gt;HTML&lt;/span&gt; produced from the &lt;span class="caps"&gt;JSP&lt;/span&gt; files &amp;#8211; is very hard to do unless the system has been deployed.&lt;/p&gt;


	&lt;p&gt;Lot&amp;#8217;s of teams fall back on Selenium, Mercury, or other tools that test GUIs &lt;em&gt;through&lt;/em&gt; the web server.  However, this leads to very fragile tests that break when the &lt;em&gt;format&lt;/em&gt; of a page changes, even though it&amp;#8217;s content remains unchanged.  Other teams solve the fragility problem by using Cactus, or the somewhat more primitive tools in &lt;code&gt;HtmlUnit&lt;/code&gt; and &lt;code&gt;HttpUnit&lt;/code&gt;, to inspect the generated &lt;span class="caps"&gt;HTML&lt;/span&gt; delivered to them by the running web application.  We&amp;#8217;ll talk about those techniques in another blog in this series.&lt;/p&gt;


	&lt;p&gt;In this article I will demonstrate a simple technique for testing &lt;span class="caps"&gt;JSP&lt;/span&gt; pages using &lt;code&gt;JUnit&lt;/code&gt; and &lt;code&gt;HtmlUnit&lt;/code&gt;, &lt;em&gt;completely outside&lt;/em&gt; the container.  The advantages of such a technique should be clear.&lt;/p&gt;


	&lt;ul&gt;
	&lt;li&gt;You don&amp;#8217;t have to have the container running, or even working.  You can test your JSPs before you have even chosen a particular webserver.  &lt;/li&gt;
		&lt;li&gt;You can spin around the edit/compile/test loop much more quickly because you don&amp;#8217;t have to redeploy after every edit.&lt;/li&gt;
		&lt;li&gt;You can build your JSPs incrementally using Test Driven Development to guide their construction.&lt;/li&gt;
	&lt;/ul&gt;


	&lt;p&gt;The reason that testing JSPs outside the container isn&amp;#8217;t more common is because JSPs are &lt;em&gt;designed&lt;/em&gt; to run inside the container.  The designers never gave a lot of thought to running them outside the container.  So the code generated by the &lt;span class="caps"&gt;JSP&lt;/span&gt; compiler depends on facilities supplied by the container.  Even the tools that generate the &lt;span class="caps"&gt;JSP&lt;/span&gt; code expect that you already have a webapp that can be deployed.  Therefore, in order to run outside the container, you somehow have to fake these facilities and tools.&lt;/p&gt;


	&lt;h3 style="color:blue;text-align:center;"&gt;Dependency Management Rant.&lt;/h3&gt;


&lt;div style="background:lightgrey;color:blue;margin-left:1cm;margin-right:1cm;font-family:times;font-size:150%"&gt;Why do so many designers of frameworks and tools expect you to live within their narrow world view?  Why must I have a fully functional web app before I can &lt;em&gt;compile&lt;/em&gt; my JSPs?  Why must they run within the container?  Information hiding has been one of the foundational tenets of good software design for decades.  When will our industry begin to take it seriously?&lt;/div&gt;

	&lt;h3&gt;Compiling JSPs&lt;/h3&gt;


	&lt;p&gt;The first step to testing JSPs is to compile them into servlets.  To do that, we first have to translate them from &lt;span class="caps"&gt;JSP&lt;/span&gt; format to &lt;span class="caps"&gt;JAVA&lt;/span&gt;.  The &lt;em&gt;apache&lt;/em&gt; project provides a tool named &lt;em&gt;Jasper&lt;/em&gt; that does this.  Invoking &lt;em&gt;Jasper&lt;/em&gt; on a &lt;span class="caps"&gt;JSP&lt;/span&gt; named &lt;code&gt;MyPage.jsp&lt;/code&gt; creates a &lt;span class="caps"&gt;JAVA&lt;/span&gt; source file named &lt;code&gt;MyPage_jsp.java&lt;/code&gt;.  This file can then be compiled into a servlet using your favorite &lt;span class="caps"&gt;IDE&lt;/span&gt;.  (Mine is IntelliJ).&lt;/p&gt;


	&lt;p&gt;Unfortunately &lt;em&gt;Jasper&lt;/em&gt; was not designed to be run from the command line.  Or rather, half of it was, and half of it wasn&amp;#8217;t.  &lt;em&gt;Jasper&lt;/em&gt; does in fact have a main function that takes standard command line arguments.  And it is &lt;em&gt;feasible&lt;/em&gt; to issue the command &lt;code&gt;java org.apache.jasper.JspC&lt;/code&gt; to invoke it.  However, &lt;em&gt;Jasper&lt;/em&gt; expects the environment it runs in to be consistent with the environment of the container.  It expects many the &lt;em&gt;apache&lt;/em&gt; JAR files to be in the &lt;span class="caps"&gt;CLASSPATH&lt;/span&gt;.  It also expects to see a &lt;code&gt;web.xml&lt;/code&gt; file that describes the web application, and it wants to see a &lt;span class="caps"&gt;WEB&lt;/span&gt;-INF directory with all the appropriate web application &lt;span class="caps"&gt;JAR&lt;/span&gt; files and &lt;span class="caps"&gt;TLD&lt;/span&gt; files present.  In short, &lt;em&gt;Jasper&lt;/em&gt; expects there to be a webapp.&lt;/p&gt;


	&lt;p&gt;To make matters worse, there appear to be bugs in the version of &lt;em&gt;Jasper&lt;/em&gt; that I am using (tomcat 5.5.20) that cause it to generate slightly incorrect code unless it is invoked in a manner that is consistent with the way that &lt;span class="caps"&gt;TOMCAT&lt;/span&gt; invokes it.&lt;/p&gt;


The first issue is inconvenient but is just a matter of creating the appropriate directories and files and then invoking &lt;em&gt;Jaser&lt;/em&gt; from &lt;span class="caps"&gt;ANT&lt;/span&gt; so that the classpaths are easy to control.  The second issue required a fair bit of research and experimentation to get working right.  But in the end, the following ant file seems to work quite nicely.  Look at the last task to see the &lt;code&gt;JspC&lt;/code&gt; invocation.
&lt;pre&gt;&lt;code&gt;
&amp;lt;project name="Library" default="compile" basedir="."&amp;gt;
  &amp;lt;property environment="env"/&amp;gt;
  &amp;lt;property name="build.home" value="${basedir}/build"/&amp;gt;
  &amp;lt;property name="build.war.home" value="${build.home}/war"/&amp;gt;
  &amp;lt;property name="build.classes.home" value="${build.home}/classes"/&amp;gt;
  &amp;lt;property name="build.jar.home" value="${build.home}/jars"/&amp;gt;
  &amp;lt;property name="catalina.home" value="${env.CATALINA_HOME}"/&amp;gt;
  &amp;lt;property name="dist.home" value="${basedir}/dist"/&amp;gt;
  &amp;lt;property name="web.home" value="${basedir}/web"/&amp;gt;

  &amp;lt;path id="compile.classpath"&amp;gt;
    &amp;lt;fileset dir="lib"&amp;gt;
      &amp;lt;include name="*.jar"/&amp;gt;
    &amp;lt;/fileset&amp;gt;
    &amp;lt;pathelement location="${catalina.home}/common/classes"/&amp;gt;
    &amp;lt;fileset dir="${catalina.home}/common/endorsed"&amp;gt;
      &amp;lt;include name="*.jar"/&amp;gt;
    &amp;lt;/fileset&amp;gt;
    &amp;lt;fileset dir="${catalina.home}/common/lib"&amp;gt;
      &amp;lt;include name="*.jar"/&amp;gt;
    &amp;lt;/fileset&amp;gt;
    &amp;lt;pathelement location="${catalina.home}/shared/classes"/&amp;gt;
    &amp;lt;fileset dir="${catalina.home}/shared/lib"&amp;gt;
      &amp;lt;include name="*.jar"/&amp;gt;
    &amp;lt;/fileset&amp;gt;
  &amp;lt;/path&amp;gt;

  &amp;lt;target name="clean"&amp;gt;
    &amp;lt;delete dir="${build.home}"/&amp;gt;
    &amp;lt;delete dir="${dist.home}"/&amp;gt;
  &amp;lt;/target&amp;gt;

  &amp;lt;target name="compile"&amp;gt;
    &amp;lt;mkdir dir="${build.classes.home}"/&amp;gt;
    &amp;lt;javac srcdir="${src.home}" destdir="${build.classes.home}" excludes="**/*Test.java"&amp;gt;
      &amp;lt;classpath refid="compile.classpath"/&amp;gt;
    &amp;lt;/javac&amp;gt;
  &amp;lt;/target&amp;gt;

  &amp;lt;target name="jar" depends="compile"&amp;gt;
    &amp;lt;mkdir dir="${build.jar.home}"/&amp;gt;
    &amp;lt;jar jarfile="${build.jar.home}/application.jar" basedir="${build.classes.home}" includes="**/application/**/*.class" /&amp;gt;
  &amp;lt;/target&amp;gt;

  &amp;lt;target name="dist" depends="jar"&amp;gt;
    &amp;lt;copy todir="${build.war.home}"&amp;gt;
      &amp;lt;fileset dir="${web.home}"/&amp;gt;
    &amp;lt;/copy&amp;gt;

    &amp;lt;copy todir="${build.war.home}/WEB-INF/lib"&amp;gt;
      &amp;lt;fileset dir="${build.jar.home}" includes="*.jar"/&amp;gt;
    &amp;lt;/copy&amp;gt;

    &amp;lt;mkdir dir="${dist.home}"/&amp;gt;
    &amp;lt;jar jarfile="${dist.home}/${app.name}.war" basedir="${build.war.home}"/&amp;gt;
  &amp;lt;/target&amp;gt;

  &amp;lt;target name="jsp" depends="dist"&amp;gt;
    &amp;lt;delete dir="${basedir}/testjsp"/&amp;gt;
    &amp;lt;java classname="org.apache.jasper.JspC" fork="true"&amp;gt;
      &amp;lt;arg line="-v -d ${basedir}/testjsp -p com.objectmentor.library.jsp -mapped -compile -webapp ${build.war.home}"/&amp;gt;
      &amp;lt;arg line="WEB-INF/pages/patrons/books/loanRecords.jsp"/&amp;gt;
      &amp;lt;classpath&amp;gt;
        &amp;lt;fileset dir="${catalina.home}/common/lib"&amp;gt;
          &amp;lt;include name="*.jar"/&amp;gt;
        &amp;lt;/fileset&amp;gt;
        &amp;lt;fileset dir="${catalina.home}/server/lib"&amp;gt;
          &amp;lt;include name="*.jar"/&amp;gt;
        &amp;lt;/fileset&amp;gt;
        &amp;lt;fileset dir="${catalina.home}/bin"&amp;gt;
          &amp;lt;include name="*.jar"/&amp;gt;
        &amp;lt;/fileset&amp;gt;
        &amp;lt;fileset dir="${build.war.home}/WEB-INF/lib"&amp;gt;
          &amp;lt;include name="*.jar"/&amp;gt;
        &amp;lt;/fileset&amp;gt;
        &amp;lt;pathelement location="/Developer/Java/Ant/lib/ant.jar"/&amp;gt;
      &amp;lt;/classpath&amp;gt;
    &amp;lt;/java&amp;gt;
    &amp;lt;jar jarfile="${build.jar.home}/jsp.jar" basedir="${basedir}/testjsp" 
         includes="**/jsp/**/*.class" 
      /&amp;gt;
  &amp;lt;/target&amp;gt;
&amp;lt;/project&amp;gt;&lt;/code&gt;&lt;/pre&gt;

	&lt;p&gt;Of course you need all the standard files and directories beneath &lt;code&gt;${build.war.home}&lt;/code&gt; to make this work.  If you are using custom tags in your JSPs, make sure you have all the appropriate &lt;span class="caps"&gt;TLD&lt;/span&gt; files in your &lt;code&gt;tld&lt;/code&gt; directory.&lt;/p&gt;


	&lt;p&gt;Notice that the &lt;span class="caps"&gt;ANT&lt;/span&gt; file invokes the JspC command line, rather than using the JspC ant task that comes with &lt;span class="caps"&gt;TOMCAT&lt;/span&gt;.  All the docs will tell you to use that ant task.  However, I found that it doesn&amp;#8217;t work well when you have custom tags.  Maybe I&amp;#8217;m just dumb, or maybe there is a bug in &lt;em&gt;Jasper&lt;/em&gt;; but the only way I found to get &lt;em&gt;Jasper&lt;/em&gt; to generate the right code was to invoke it from the command line &lt;em&gt;and explicitly pass the &lt;span class="caps"&gt;JSP&lt;/span&gt; files in as command line argument!&lt;/em&gt;.  If you depend on either the ant task or the command line to scan the whole web app for all the &lt;span class="caps"&gt;JSP&lt;/span&gt; files, it seems to generate the wrong code.  (See: &lt;a href="http://blog.objectmentor.com/articles/2007/02/08/jasper-problem-resolved"&gt;this blog&lt;/a&gt;)&lt;/p&gt;


	&lt;p&gt;Now that we have a &lt;span class="caps"&gt;JAVA&lt;/span&gt; file, let&amp;#8217;s take a look at it.  First, here&amp;#8217;s the &lt;span class="caps"&gt;JSP&lt;/span&gt; file.&lt;/p&gt;


&lt;pre&gt;&lt;code&gt;&amp;lt;%@ page import="com.objectmentor.library.utils.DateUtil" %&amp;gt;
&amp;lt;%@ page import="com.objectmentor.library.web.controller.patrons.LoanRecord" %&amp;gt;
&amp;lt;%@ page import="java.util.List" %&amp;gt;
&amp;lt;%
  List loanRecords = (List) request.getAttribute("loanRecords");
  if (loanRecords.size() &amp;gt; 0) {
%&amp;gt;
&amp;lt;table class="list" id="loanRecords"&amp;gt;
  &amp;lt;tr&amp;gt;
    &amp;lt;th&amp;gt;ID&amp;lt;/th&amp;gt;
    &amp;lt;th&amp;gt;Title&amp;lt;/th&amp;gt;
    &amp;lt;th&amp;gt;Due date&amp;lt;/th&amp;gt;
    &amp;lt;th&amp;gt;Fine&amp;lt;/th&amp;gt;
  &amp;lt;/tr&amp;gt;
  &amp;lt;%
    for (int i = 0; i &amp;lt; loanRecords.size(); i++) {
      LoanRecord loanRecord = (LoanRecord) loanRecords.get(i);
  %&amp;gt;
  &amp;lt;tr class="&amp;lt;%=i%2==0?"even":"odd"%&amp;gt;"&amp;gt;
    &amp;lt;td&amp;gt;&amp;lt;%=loanRecord.id%&amp;gt;
    &amp;lt;/td&amp;gt;
    &amp;lt;td&amp;gt;&amp;lt;%=loanRecord.title%&amp;gt;
    &amp;lt;/td&amp;gt;
    &amp;lt;td&amp;gt;&amp;lt;%=DateUtil.dateToString(loanRecord.dueDate)%&amp;gt;
    &amp;lt;/td&amp;gt;
    &amp;lt;td&amp;gt;&amp;lt;%=loanRecord.fine.toString()%&amp;gt;
    &amp;lt;/td&amp;gt;
  &amp;lt;/tr&amp;gt;
  &amp;lt;%
    }
  %&amp;gt;
&amp;lt;/table&amp;gt;
&amp;lt;%
  }
%&amp;gt;&lt;/code&gt;&lt;/pre&gt;

	&lt;p&gt;And here is the code that &lt;em&gt;Jasper&lt;/em&gt; created from this file.&lt;/p&gt;


&lt;pre&gt;&lt;code&gt;package com.objectmentor.library.jsp.WEB_002dINF.pages.patrons.books;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.jsp.*;
import com.objectmentor.library.utils.DateUtil;
import com.objectmentor.library.web.controller.patrons.LoanRecord;
import java.util.List;

public final class loanRecords_jsp extends org.apache.jasper.runtime.HttpJspBase
    implements org.apache.jasper.runtime.JspSourceDependent {

  private static java.util.List _jspx_dependants;

  public Object getDependants() {
    return _jspx_dependants;
  }

  public void _jspService(HttpServletRequest request, HttpServletResponse response)
        throws java.io.IOException, ServletException {

    JspFactory _jspxFactory = null;
    PageContext pageContext = null;
    HttpSession session = null;
    ServletContext application = null;
    ServletConfig config = null;
    JspWriter out = null;
    Object page = this;
    JspWriter _jspx_out = null;
    PageContext _jspx_page_context = null;

    try {
      _jspxFactory = JspFactory.getDefaultFactory();
      response.setContentType("text/html");
      pageContext = _jspxFactory.getPageContext(this, request, response,
                  null, true, 8192, true);
      _jspx_page_context = pageContext;
      application = pageContext.getServletContext();
      config = pageContext.getServletConfig();
      session = pageContext.getSession();
      out = pageContext.getOut();
      _jspx_out = out;

      out.write('\n');
      out.write('\n');
      out.write('\n');

  List loanRecords = (List) request.getAttribute("loanRecords");
  if (loanRecords.size() &amp;gt; 0) {

      out.write("\n");
      out.write("&amp;lt;table class=\"list\" id=\"loanRecords\"&amp;gt;\n");
      out.write("  &amp;lt;tr&amp;gt;\n");
      out.write("    &amp;lt;th&amp;gt;ID&amp;lt;/th&amp;gt;\n");
      out.write("    &amp;lt;th&amp;gt;Title&amp;lt;/th&amp;gt;\n");
      out.write("    &amp;lt;th&amp;gt;Due date&amp;lt;/th&amp;gt;\n");
      out.write("    &amp;lt;th&amp;gt;Fine&amp;lt;/th&amp;gt;\n");
      out.write("  &amp;lt;/tr&amp;gt;\n");
      out.write("  ");

    for (int i = 0; i &amp;lt; loanRecords.size(); i++) {
      LoanRecord loanRecord = (LoanRecord) loanRecords.get(i);

      out.write("\n");
      out.write("  &amp;lt;tr class=\"");
      out.print(i%2==0?"even":"odd");
      out.write("\"&amp;gt;\n");
      out.write("    &amp;lt;td&amp;gt;");
      out.print(loanRecord.id);
      out.write("\n");
      out.write("    &amp;lt;/td&amp;gt;\n");
      out.write("    &amp;lt;td&amp;gt;");
      out.print(loanRecord.title);
      out.write("\n");
      out.write("    &amp;lt;/td&amp;gt;\n");
      out.write("    &amp;lt;td&amp;gt;");
      out.print(DateUtil.dateToString(loanRecord.dueDate));
      out.write("\n");
      out.write("    &amp;lt;/td&amp;gt;\n");
      out.write("    &amp;lt;td&amp;gt;");
      out.print(loanRecord.fine.toString());
      out.write("\n");
      out.write("    &amp;lt;/td&amp;gt;\n");
      out.write("  &amp;lt;/tr&amp;gt;\n");
      out.write("  ");

    }

      out.write("\n");
      out.write("&amp;lt;/table&amp;gt;\n");

  }

    } catch (Throwable t) {
      if (!(t instanceof SkipPageException)){
        out = _jspx_out;
        if (out != null &amp;#38;&amp;#38; out.getBufferSize() != 0)
          out.clearBuffer();
        if (_jspx_page_context != null) _jspx_page_context.handlePageException(t);
      }
    } finally {
      if (_jspxFactory != null) _jspxFactory.releasePageContext(_jspx_page_context);
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

	&lt;h3 style="color:blue;text-align:center;"&gt;Rant About Final.&lt;/h3&gt;


&lt;div style="background:lightgrey;color:blue;margin-left:1cm;margin-right:1cm;font-family:times;font-size:150%"&gt;Why is this class declared &lt;code&gt;final&lt;/code&gt;?  What if I had wanted to derive from it to create a testing stub?  Why would &lt;em&gt;anyone&lt;/em&gt; think that generated code is so sacrosanct that I wouldn&amp;#8217;t want to override it.&lt;/div&gt;

	&lt;p&gt;&lt;br/&gt;
A quick perusal of this code tells us that in order to use an instance of this servlet we need an &lt;code&gt;HttpServletRequest&lt;/code&gt; instance, and an &lt;code&gt;HttpServletResponse&lt;/code&gt; instance.&lt;/p&gt;


	&lt;p&gt;Looking a little closer we find that the servlet writes all the &lt;span class="caps"&gt;HTML&lt;/span&gt; to an instance of &lt;code&gt;JspWriter&lt;/code&gt; that it gets from something called a &lt;code&gt;PageContext&lt;/code&gt;.  So, if we could create a mocked up version of &lt;code&gt;JspWriter&lt;/code&gt; that saves all this &lt;span class="caps"&gt;HTML&lt;/span&gt;, and a mocked up version of &lt;code&gt;PageContext&lt;/code&gt; that delivers the mocked &lt;code&gt;JspWriter&lt;/code&gt;, then we could access the written &lt;span class="caps"&gt;HTML&lt;/span&gt; from our tests.&lt;/p&gt;


	&lt;p&gt;Fortunately the &lt;span class="caps"&gt;TOMCAT&lt;/span&gt; designers decoupled the creation of the &lt;code&gt;JspWriter&lt;/code&gt; into a factory named &lt;code&gt;JspFactory&lt;/code&gt;.  That factory can be overridden!  This means that we can get our mocked up &lt;code&gt;JspWriter&lt;/code&gt; into the servlet without modifying the servlet.  All we need is something like the following code:&lt;/p&gt;


&lt;pre&gt;&lt;code&gt;  class MockJspFactory extends JspFactory {
    public PageContext getPageContext(Servlet servlet, ServletRequest servletRequest, ServletResponse servletResponse, String string, boolean b, int i, boolean b1) {
      return new MockPageContext(new MockJspWriter());
    }

    public void releasePageContext(PageContext pageContext) {
    }

    public JspEngineInfo getEngineInfo() {
      return null;
    }
  }&lt;/code&gt;&lt;/pre&gt;

	&lt;p&gt;Now we need the mocked &lt;code&gt;JspWriter&lt;/code&gt;.  For the purposes of this demonstration, I used the following:&lt;/p&gt;


	&lt;h3 style="font-family:courier;text-align:center;"&gt;MockJspWriter&lt;/h3&gt;


&lt;pre&gt;&lt;code&gt;package com.objectmentor.library.web.framework.mocks;

import javax.servlet.jsp.JspWriter;
import java.io.IOException;

public class MockJspWriter extends JspWriter {

  private StringBuffer submittedContent;

  public MockJspWriter(int bufferSize, boolean autoFlush) {
    super(bufferSize, autoFlush);
    submittedContent = new StringBuffer();
  }

  public String getContent() {
    return submittedContent.toString();
  }

  public void print(String arg0) throws IOException {
    submittedContent.append(arg0);
  }

  public void write(char[] arg0, int arg1, int arg2) throws IOException {
    for (int i=0; i&amp;lt;arg2; i++)
      submittedContent.append(String.valueOf(arg0[arg1++]));
  }

  public void write(String content) throws IOException {
    submittedContent.append(content);
  }

  // lots of uninteresting methods elided.  I just gave them
  // degenerate implementations.  (e.g. {})
}
&lt;/code&gt;&lt;/pre&gt;

	&lt;p&gt;Don&amp;#8217;t be concerned about the comment regarding all the unimplemented methods I elided.  I&amp;#8217;ve taken the attitude that I will only implement those methods that I need to get my tests to work.  The others I leave with degenerate implementations.&lt;/p&gt;


	&lt;p&gt;My &lt;span class="caps"&gt;IDE&lt;/span&gt; was very helpful in creating these mocks.  It automatically builds method prototypes and degenerate implementations for all the methods of an interface or abstract class that need implementing.&lt;/p&gt;


	&lt;p&gt;The &lt;code&gt;MockPageContext&lt;/code&gt;, &lt;code&gt;MockHttpServletRequest&lt;/code&gt; and &lt;code&gt;MockHttServletResponse&lt;/code&gt; classes were created in a similar way.&lt;/p&gt;


	&lt;h3 style="font-family:courier;text-align:center;"&gt;MockPageContext&lt;/h3&gt;


&lt;pre&gt;&lt;code&gt;package com.objectmentor.library.web.framework.mocks;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.jsp.*;
import java.io.IOException;
import java.util.Enumeration;

public class MockPageContext extends PageContext {

  private final JspWriter out;
  private HttpServletRequest request;

  public MockPageContext(JspWriter out) {
    this.out = out;
    request = new MockHttpServletRequest();
  }

  public JspWriter getOut() {
    return out;
  }

  public ServletRequest getRequest() {
    return request;
  }
  // lots of degenerate functions elided.
}
&lt;/code&gt;&lt;/pre&gt;

	&lt;h3 style="font-family:courier;text-align:center;"&gt;MockHttpServletRequest&lt;/h3&gt;


&lt;pre&gt;&lt;code&gt;package com.objectmentor.library.web.framework.mocks;

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.security.Principal;
import java.util.*;

public class MockHttpServletRequest implements HttpServletRequest {

  private String method;
  private String contextPath;
  private String requestURI;
  private HttpSession session = new MockHttpSession();
  private Map parameters = new HashMap();
  private Map attributes = new HashMap();

  public MockHttpServletRequest(String method, String contextPath,
                                String requestURI) {
    super();
    this.method = method;
    this.contextPath = contextPath;
    this.requestURI = requestURI;
  }

  public MockHttpServletRequest() {
    this("GET");
  }

  public MockHttpServletRequest(String method) {
    this(method, "/Library", "/Library/foo/bar.jsp");
  }

  public String getContextPath() {
    return contextPath;
  }

  public String getMethod() {
    return method;
  }

  public String getRequestURI() {
    return requestURI;
  }

  public String getServletPath() {
    return requestURI.substring(getContextPath().length());
  }

  public HttpSession getSession() {
    return session;
  }

  public HttpSession getSession(boolean arg0) {
    return session;
  }

  public Object getAttribute(String arg0) {
    return attributes.get(arg0);
  }

  public String getParameter(String arg0) {
    return (String) parameters.get(arg0);
  }

  public Map getParameterMap() {
    return parameters;
  }

  public Enumeration getParameterNames() {
    return null;
  }

  public void setSession(HttpSession session) {
    this.session = session;
  }

  public void setParameter(String s, String s1) {
    parameters.put(s, s1);
  }

  public void setAttribute(String name, Object value) {
    attributes.put(name, value);
  }

  // Lots of degenerate methods elided.
}
&lt;/code&gt;&lt;/pre&gt;

	&lt;h3 style="font-family:courier;text-align:center;"&gt;MockHttpServletResponse&lt;/h3&gt;


&lt;pre&gt;&lt;code&gt;package com.objectmentor.library.web.framework.mocks;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.*;
import java.io.*;
import java.util.Locale;

public class MockHttpServletResponse implements HttpServletResponse {
  // all functions are implemented to be degenerate.
}
&lt;/code&gt;&lt;/pre&gt;

	&lt;p&gt;Given these mock objects, I can now create an instance of my &lt;code&gt;loanRecords_jsp&lt;/code&gt; servlet and start calling methods on it!  Indeed, my first test case looked something like this:&lt;/p&gt;


&lt;pre&gt;&lt;code&gt;  public void testSimpleTest() throws Exception {
    MockJspWriter jspWriter = new MockJspWriter();
    MockPageContext pageContext = new MockPageContext(jspWriter);
    JspFactory.setDefaultFactory(new MockJspFactory(pageContext));
    HttpJspBase jspPage = new loanRecords_jsp();
    HttpServletRequest request = new MockHttpServletRequest();
    HttpServletResponse response = new MockHttpServletResponse();

    jspPage._jspInit();
    jspPage._jspService(request, response);

    assertEquals("", jspWriter.getContent());
  }&lt;/code&gt;&lt;/pre&gt;

	&lt;p&gt;The test fails, as expected, because the content is a bit more than blank &amp;#8211; but not much more.  If you look carefully at the &lt;span class="caps"&gt;JSP&lt;/span&gt; file you&amp;#8217;ll see that it calls &lt;code&gt;request.getAttribute("loanRecords")&lt;/code&gt; and expects a List back.  But since our test did not create such an attribute, the code throws an exception that is silently caught.&lt;/p&gt;


	&lt;p&gt;To get real &lt;span class="caps"&gt;HTML&lt;/span&gt; out of this servlet, we need to load up the attributes.  Then we can use HtmlUnit to parse that &lt;span class="caps"&gt;HTML&lt;/span&gt; and write tests against it.&lt;/p&gt;


	&lt;p&gt;HtmlUnit is very simple to use, especially for testing generated web pages like this.  There are a few gotcha&amp;#8217;s that I described &lt;a href="http://blog.objectmentor.com/articles/2007/02/11/dependency-management-httpunit"&gt;here&lt;/a&gt;&lt;/p&gt;


	&lt;p&gt;Here is the final test that loads the attributes, inspects the &lt;span class="caps"&gt;HTML&lt;/span&gt; with htmlUnit, and passes appropriately:&lt;/p&gt;


&lt;pre&gt;&lt;code&gt;package com.objectmentor.library.jspTest.books.patrons.books;

import com.gargoylesoftware.htmlunit.*;
import com.gargoylesoftware.htmlunit.html.*;
import com.objectmentor.library.jsp.WEB_002dINF.pages.patrons.books.loanRecords_jsp;
import com.objectmentor.library.utils.*;
import com.objectmentor.library.web.controller.patrons.LoanRecord;
import com.objectmentor.library.web.framework.mocks.*;
import junit.framework.TestCase;
import org.apache.jasper.runtime.HttpJspBase;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.jsp.*;
import java.util.*;

public class LoanRecordsJspTest extends TestCase {
  private MockPageContext pageContext;
  private MockJspWriter jspWriter;
  private JspFactory mockFactory;
  private MockHttpServletResponse response;
  private MockHttpServletRequest request;
  private WebClient webClient;
  private TopLevelWindow dummyWindow;

  protected void setUp() throws Exception {
    jspWriter = new MockJspWriter();
    pageContext = new MockPageContext(jspWriter);
    mockFactory = new MockJspFactory(pageContext);

    JspFactory.setDefaultFactory(mockFactory);
    response = new MockHttpServletResponse();
    request = new MockHttpServletRequest();
    webClient = new WebClient();
    webClient.setJavaScriptEnabled(false);
    dummyWindow = new TopLevelWindow("", webClient);
  }

  public void testLoanRecordsPageGeneratesAppropriateTableRows() throws Exception {
    HttpJspBase jspPage = new loanRecords_jsp();
    jspPage._jspInit();

    List&amp;lt;LoanRecord&amp;gt; loanRecords = new ArrayList&amp;lt;LoanRecord&amp;gt;();
    addLoanRecord(loanRecords,
                  "99",
                  "Empire",
                  DateUtil.dateFromString("2/11/2007"),
                  new Money(4200));
    addLoanRecord(loanRecords,
                  "98",
                  "Orbitsville",
                  DateUtil.dateFromString("2/12/2007"),
                  new Money(5200));

    request.setAttribute("loanRecords", loanRecords);

    jspPage._jspService(request, response);

    StringWebResponse stringWebResponse = new StringWebResponse(jspWriter.getContent());
    HtmlPage page = HTMLParser.parse(stringWebResponse, dummyWindow);
    HtmlElement html = page.getDocumentElement();

    HtmlTable table = (HtmlTable) html.getHtmlElementById("loanRecords");
    List&amp;lt;HtmlTableRow&amp;gt; rows = table.getHtmlElementsByTagName("tr");
    assertEquals(3, rows.size());

    assertEquals("even", classOfElement(rows.get(1)));
    assertEquals("odd", classOfElement(rows.get(2)));

    List&amp;lt;HtmlTableDataCell&amp;gt; firstRowCells = rows.get(1).getCells();
    assertEquals(4, firstRowCells.size());

    List&amp;lt;HtmlTableDataCell&amp;gt; secondRowCells = rows.get(2).getCells();
    assertEquals(4, secondRowCells.size());

    assertLoanRecordRowEquals("99", "Empire", "02/11/2007", "$42.00", firstRowCells);
    assertLoanRecordRowEquals("98", "Orbitsville", "02/12/2007", "$52.00", secondRowCells);
  }

  private String classOfElement(HtmlTableRow firstDataRow) {return firstDataRow.getAttributeValue("class");}

  private void assertLoanRecordRowEquals(String id, String title, String dueDate, String fine, List&amp;lt;HtmlTableDataCell&amp;gt; rowCells) {
    assertEquals(id, rowCells.get(0).asText());
    assertEquals(title, rowCells.get(1).asText());
    assertEquals(dueDate, rowCells.get(2).asText());
    assertEquals(fine, rowCells.get(3).asText());
  }

  private void addLoanRecord(List&amp;lt;LoanRecord&amp;gt; loanRecords, String id, String title, Date dueDate, Money fine) {
    LoanRecord loanRecord = new LoanRecord();
    loanRecord.id = id;
    loanRecord.title = title;
    loanRecord.dueDate = dueDate;
    loanRecord.fine = fine;

    loanRecords.add(loanRecord);
  }

  private class MockJspFactory extends JspFactory {
    private PageContext pageContext;
    public MockJspFactory(PageContext pageContext) {
      this.pageContext = pageContext;
    }

    public PageContext getPageContext(Servlet servlet, ServletRequest servletRequest, ServletResponse servletResponse, String string, boolean b, int i, boolean b1) {
      return pageContext;
    }

    public void releasePageContext(PageContext pageContext) {
    }

    public JspEngineInfo getEngineInfo() {
      return null;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

	&lt;p&gt;This test makes sure that the generated &lt;span class="caps"&gt;HTML&lt;/span&gt; has the appropriate content contained within the rows of a table.  The test &lt;em&gt;does&lt;/em&gt; expect to see a table, and expects the rows to appear in the appropriate order.  It also ensures that the rows will have alternating styles.  Other than that, all issues of form and syntax are ignored.&lt;/p&gt;


	&lt;h3&gt;Conclusion&lt;/h3&gt;


	&lt;p&gt;The technique described here can be used to test virtually any static web page, or portion thereof &lt;em&gt;outside&lt;/em&gt; of a container, and without a webserver running.  It is relatively simple to set up; and then very easy to extend.  With it, you can spin around the edit/compile/test loop very quickly, and can easily follow the rules of Test Driven Development.&lt;/p&gt;</description>
      <pubDate>Sun, 11 Feb 2007 20:54:37 +0000</pubDate>
      <guid isPermaLink="false">urn:uuid:be6396f8-d544-4aab-9520-8bc8a5ff3eeb</guid>
      <author>Uncle Bob</author>
      <link>http://blog.objectmentor.com/articles/2007/02/11/testing-guis-part-ii-jsp</link>
      <category>Uncle Bob's Blatherings</category>
      <category>Testing GUIs</category>
      <trackback:ping>http://blog.objectmentor.com/articles/trackback/162</trackback:ping>
    </item>
    <item>
      <title>"Testing GUIs Part II: JSP" by Aaron</title>
      <description>&lt;p&gt;Uncle Bob,&lt;/p&gt;


	&lt;p&gt;When will you finish your next blog in this series? We, engineers working in hp, are really looking forward to see that:-)&lt;/p&gt;</description>
      <pubDate>Mon, 02 Jul 2007 21:06:53 +0000</pubDate>
      <guid isPermaLink="false">urn:uuid:4b530f50-46db-439b-abca-e69773b8bb15</guid>
      <link>http://blog.objectmentor.com/articles/2007/02/11/testing-guis-part-ii-jsp#comment-468</link>
    </item>
    <item>
      <title>"Testing GUIs Part II: JSP" by Aaron</title>
      <description>&lt;p&gt;May be you miss one thing &amp;#8211; There should be a default constructor exists within class MockJspWriter:&lt;/p&gt;


	&lt;pre&gt;&lt;code&gt;private final static int BUFF_SIZE = 1024;&lt;/code&gt;&lt;/pre&gt;


	&lt;pre&gt;&lt;code&gt;public MockJspWriter()
{
    super(BUFF_SIZE, true);
}&lt;/code&gt;&lt;/pre&gt;</description>
      <pubDate>Thu, 14 Jun 2007 03:17:24 +0000</pubDate>
      <guid isPermaLink="false">urn:uuid:210004d3-1f79-492a-9d67-6b296a355a57</guid>
      <link>http://blog.objectmentor.com/articles/2007/02/11/testing-guis-part-ii-jsp#comment-421</link>
    </item>
    <item>
      <title>"Testing GUIs Part II: JSP" by Maomao</title>
      <description>&lt;p&gt;Uncle Bob,&lt;/p&gt;


	&lt;p&gt;This is really an invigorative thing. 
I am really looking forward of this series that cover in detail, for instance, what if there is a  inside, and some other situations.&lt;/p&gt;</description>
      <pubDate>Fri, 25 May 2007 01:45:22 +0000</pubDate>
      <guid isPermaLink="false">urn:uuid:d5e91d1a-4023-4040-bb5d-706035160a4b</guid>
      <link>http://blog.objectmentor.com/articles/2007/02/11/testing-guis-part-ii-jsp#comment-212</link>
    </item>
    <item>
      <title>"Testing GUIs Part II: JSP" by GregJ</title>
      <description>&lt;p&gt;Re: Rant About Final.&lt;/p&gt;


	&lt;p&gt;&amp;gt; Why is this class declared final?&lt;/p&gt;


	&lt;p&gt;There are performance issues.&lt;/p&gt;


	&lt;p&gt;See also: &lt;a href="http://www.javaperformancetuning.com/tips/final.shtml" rel="nofollow"&gt;http://www.javaperformancetuning.com/tips/final.shtml&lt;/a&gt;&lt;/p&gt;</description>
      <pubDate>Fri, 23 Feb 2007 19:28:06 +0000</pubDate>
      <guid isPermaLink="false">urn:uuid:fca49b49-bc8f-4784-9385-4733179cbe87</guid>
      <link>http://blog.objectmentor.com/articles/2007/02/11/testing-guis-part-ii-jsp#comment-126</link>
    </item>
    <item>
      <title>"Testing GUIs Part II: JSP" by DaveB</title>
      <description>&lt;p&gt;Really interesting article.
I did this for a spring mvc app.  I got the same code as tomcat however, when I run it I get an exception (which I make the mock propogate up).&lt;/p&gt;


	&lt;p&gt;Caused by: java.lang.NoSuchMethodError: javax.servlet.jsp.PageContext.getVariableResolver()Ljavax/servlet/jsp/el/VariableResolver;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;at org.apache.jasper.runtime.PageContextImpl.proprietaryEvaluate(PageContextImpl.java:917)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;at&lt;/p&gt;


	&lt;p&gt;Any ideas?  Does this mean that I&amp;#8217;m running the test with the wrong things in the classpath?  This wouldn&amp;#8217;t normally happen (in tomcat) else the page would render nothing.&lt;/p&gt;


	&lt;p&gt;The offending line in the compiled jsp is: org.apache.jasper.runtime.PageContextImpl.proprietaryEvaluate(&amp;#8221;${myVariable}&amp;#8221;. blah, blah);&lt;/p&gt;</description>
      <pubDate>Thu, 15 Feb 2007 12:05:18 +0000</pubDate>
      <guid isPermaLink="false">urn:uuid:3969fb20-725c-4ad3-8702-2d26cdf1c1a2</guid>
      <link>http://blog.objectmentor.com/articles/2007/02/11/testing-guis-part-ii-jsp#comment-110</link>
    </item>
    <item>
      <title>"Testing GUIs Part II: JSP" by Uncle Bob</title>
      <description>&lt;p&gt;Brian,&lt;/p&gt;


	&lt;p&gt;Never fear.  I&amp;#8217;ll also be doing a blog in this series on Velocity.  As for JSP, I&amp;#8217;m sorry to have to tell you this but one of the corrolaries to Murphy&amp;#8217;s law is: &lt;em&gt;The good things you do will pass away, but your mistakes live on forever.&lt;/em&gt;&lt;/p&gt;</description>
      <pubDate>Mon, 12 Feb 2007 13:37:54 +0000</pubDate>
      <guid isPermaLink="false">urn:uuid:3705378d-1062-4881-bbf0-3d4b59f594a1</guid>
      <link>http://blog.objectmentor.com/articles/2007/02/11/testing-guis-part-ii-jsp#comment-103</link>
    </item>
    <item>
      <title>"Testing GUIs Part II: JSP" by Dean Wampler</title>
      <description>&lt;p&gt;Brian (and other readers),&lt;/p&gt;


	&lt;p&gt;Do you have any experience with alternative Java web frameworks, like Tapestry, Cocoon, etc. I&amp;#8217;m curious to hear other opinions about alternative frameworks. Thx.&lt;/p&gt;</description>
      <pubDate>Mon, 12 Feb 2007 09:29:51 +0000</pubDate>
      <guid isPermaLink="false">urn:uuid:a2133442-a94b-48bc-8a5a-f6691fad7169</guid>
      <link>http://blog.objectmentor.com/articles/2007/02/11/testing-guis-part-ii-jsp#comment-102</link>
    </item>
    <item>
      <title>"Testing GUIs Part II: JSP" by Brian Slesinsky</title>
      <description>&lt;p&gt;No, no, what are you doing?  Didn&amp;#8217;t you see the &amp;#8220;do not revive&amp;#8221; request?  JSP must die.&lt;/p&gt;</description>
      <pubDate>Sun, 11 Feb 2007 22:08:29 +0000</pubDate>
      <guid isPermaLink="false">urn:uuid:57a1db5a-df5d-4dc6-aa36-dc7878d6f004</guid>
      <link>http://blog.objectmentor.com/articles/2007/02/11/testing-guis-part-ii-jsp#comment-101</link>
    </item>
  </channel>
</rss>
