Develop web One of the most annoying things about applications is that you have to deploy them in order to test them. Of course, not all parts are like this. If you design carefully, you can test the business logic in a Java program. You can test data access, interfaces, and stored procedures without the application server running. However, if you are testing the GUI (HTML generated by Jsp), you must deploy it before you can test it.
Many teams turn to Sellenium, Mercury or other tools to test GUIs through web servers. However, even if the content of the page remains the same but the style changes, the test will become fragile. Other teams use Cactus to address this vulnerability, or primitive tools like HtmlUnit and HttpUnit to monitor the HTML generated by web applications. I will discuss these issues in another series of blogs.
In this article I will introduce a simple and easy technology that uses JUnit or HtmlUnit to test Jsp pages and is completely separated from the container. This is also the advantage of this technology.
You don't have to keep the container running or even existing. You can test your Jsp before choosing a specific webserver.
You don’t have to redeploy after every modification, so the editing/compiling/testing process will be faster.
You can use test-first development to continuously build Jsp.
The reason why Jsp testing technology outside the container is not popular is because Jsp is designed to run in the container. The designers never thought much about the possibility of running outside the container. Therefore, the code generated by the Jsp compiler often relies on many components provided by the container. Even tools that generate JSP code assume that you already have a successfully deployed web application running. So, in order to run outside of containers, you have to develop these tools and components.
#Why do the designers of so many frameworks and tools always expect you to Living in the small world they offer? Why do I have to build a complete web application before compiling JSP? Why do these things have to run in containers? Information hiding has been a basic tenet of good software design as early as 10 years ago. When will our industry take this seriously?
The first step in testing a Jsp is to compile it into a servlet. To achieve this step, we also need to convert Jsp into Java format first. Apache provides a tool called Jasper. We call Jasper to create a Java format source file MyPage_jsp.java for MyPage.jsp. You can then compile this file into a Servlet using your favorite IDE.
Unfortunately, Jasper is not designed to be used in the command line, or it is not designed exactly that way. But Jasper does have a main function for processing command line parameters, and it can be easily called by calling java org.apache.jasper.JspC. However, Jasper expects the environment it runs in to be consistent with the container environment. You need to make sure that there are a lot of Apache Jar files on the classpath, and that it can find the web application's web.xml. It also needs to be able to find the WEB-INF directory that contains the web application Jar as well as TLD files etc. In short, Jasper needs to be able to find a complete web application.
If things are worse, there are some bugs in some specific Jasper versions (I am using tomcat 5.5.20) unless it is completely consistent with the way TOMCAT is called. The code it generates will have some errors.
The first thing to do is tedious but relatively simple. You need to create the correct directory and file structure, and then call Jasper in Ant (Classpath is easier to control). The second point is that it requires some research and testing to make it work. The following is the ant file that can be run successfully. The call to JspC appears in the last task.
#
##
###
##
#
includes="**/jsp/** /*.class"
##/>##
#Of course, you want all the criteria The files and directories are under ${build.war.home} to ensure working. If you use custom tags in your Jsp, also make sure that all corresponding TLD files are in your TLD directory.
It should be noted that the command line of Jspc is called in the ant file instead of using the JspC Ant Task provided by Tomcat. Because I found that it doesn't work correctly when you have custom tags. Maybe I'm confused, or there is indeed a bug in JspC. But the only way I've found to get Jasper to generate correct code is to call it from the command line and explicitly pass the Jsp file path as a command line argument! If you rely on its Ant Task or use the command line to search for Jsps in all web applications for compilation, it will generate incorrect code. (See this blog)
Now that we have the Java file, let’s analyze it. First, please take a look at the Jsp file below.
##<%@ page import="com.objectmentor.library.utils.DateUtil" %>
<%@ page import="com.objectmentor.library.web.controller.patrons.LoanRecord" %>
<% @ page import="java.util.List" %>
<%
List loanRecords = (List) request.getAttribute("loanRecords");
##if (loanRecords.size() > 0) {
%>
##
##
# #
##
<%
##for (int i = 0; i < loanRecords.size(); i++) {
LoanRecord loanRecord = (LoanRecord) loanRecords.get(i);
%>
<%
}
%>
ID | Title | Due date | Fine |
---|---|---|---|
<%=loanRecord.id%> |
<%=loanRecord.title%> |
<%=DateUtil.dateToString(loanRecord.dueDate)%> |
<%=loanRecord.fine.toString()%> |
<%
}
%>
下面则是Jasper所生成的代码。
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() > 0) {
out.write("/n");
out.write("
out.write("
out.write("
out.write("
out.write("
out.write("
out.write("
out.write("");
for (int i = 0; i < loanRecords.size(); i++) {
LoanRecord loanRecord = (LoanRecord) loanRecords.get(i);
out.write("/n");
out.write("
out.print(i%2==0?"even":"odd");
out.write("/">/n");
out.write("
out.write("
out.write("
out.write("
out.write("
out.write("");
}
out.write("/n");
out.write("
ID | /n");Title | /n");Due date | /n");Fine | /n");
---|---|---|---|
"); out.print(loanRecord.id); out.write("/n"); out.write(" | /n");
"); out.print(loanRecord.title); out.write("/n"); out.write(" | /n");
"); out.print(DateUtil.dateToString(loanRecord.dueDate)); out.write("/n"); out.write(" | /n");
"); out.print(loanRecord.fine.toString()); out.write("/n"); out.write(" | /n");
}
} catch (Throwable t) {
if (!(t instanceof SkipPageException)){
out = _jspx_out;
if (out != null && out.getBufferSize() != 0)
out.clearBuffer();
if (_jspx_page_context != null) _jspx_page_context.handlePageException(t);
if (_jspxFactory != null) _jspxFactory.releasePageContext(_jspx_page_context);
}
}
}
##Final Complaint
#Why should this class be declared final? What if I want to create a test stub derived class? Why would anyone find a generated class so offensive that I can't even override it?
If we study it more carefully, we will find that the servlet writes all HTML to the instance of JspWriter, and JspWriter is obtained from the PageContext. If we could create a mock up version of the JspWriter to hold all this HTML, and then create a mock up version of the PageContext to dispatch the mock JspWriter, then we would be able to access this HTML in our tests.
Fortunately, the designers of Tomcat put the creation of JspWriter into the factory class of JspFactory. And this factory class can be overridden! This means that we can get our own JspWriter class in the servlet without changing the servlet. All you need is the following code.
#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;
##}
##}
Now, what we need is mock Jspwriter. For ease of presentation, I used the following:
##MockJspWriter
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
}
public void write(String content) throws IOException {
##submittedContent.append(content);
}
#// lots of uninteresting methods elied.I just gave them
##// degenerate implementations.(e.g. {})
}
No need to care about the unimplemented methods that I omitted, I think you only need to care about those Just enough to make my tests run. For the rest, I'll just use its degenerate implementation.
My IDE is very helpful in creating these mock classes. It can automatically build method prototypes and provide degenerate implementations for methods that need to be implemented by interfaces or abstract classes.
Similarly create the MockPageContext, MockHttpServletRequest and MockHttpServletResponse classes using similar methods.
##MockPageContext
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.
}
MockHttpServletRequest
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.
}
MockHttpServletResponse
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.
}
有了这些mock对象,现在我就可以创建一个loanRecords_jsp的servlet实例并且开始调用它!我的头一个测试用例就像下面这样:
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());
}
就像预期的一样,测试失败了。这是因为还有些内容还没补充上,不过所剩无多。如果你仔细的看过Jsp文件,你就会发现它调用了request.getAttribute(“loanRecords”)并且期望返回一个List。但因为目前的测试并未为这样的属性赋值,从而导致了代码抛出了异常。
要想成功让servlet输出HTML,我们还需要加载这个属性。然后,我们就可以使用HtmlUnit来解析此HTML并且编写相应的单元测试。
HtmlUnit非常的容易使用,尤其是在测试所产生的像是本例这样的web pages上。我这里还有篇文章详细的介绍了它。
下面就是最终测试加载属性的测试,它通过htmlunit来检测HTML,并且做出正确的判断:
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();
ListloanRecords = new ArrayList (); addLoanRecord(loanRecords,Copy after loginCopy after login
"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");
Listrows = table.getHtmlElementsByTagName("tr");
assertEquals(3, rows.size());
assertEquals("even", classOfElement(rows.get(1)));
assertEquals("odd", classOfElement(rows.get(2)));
ListfirstRowCells = rows.get(1).getCells();
assertEquals(4, firstRowCells.size());
ListsecondRowCells = 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, ListrowCells) {
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(ListloanRecords, 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;
}
}
}
上述的测试确保了所生成的HTML中表格中的每一行都具有正确的内容。这项测试确实能够测出是否存在这样的表格,并且判断出是否表格的每一行是按照正确的顺序来展现的。同时,它也确保了每一行的相应style。测试忽略了此外的表单以及语法部分。
这篇发表在此的技术能够用来测试几乎所有目前我们所见过的web页面,并且脱离容器,也无需web server的运行。相对来说,它也比较容易去设置,并且非常易于扩展。有了它,你就可以快速的进行编辑、编译、测试的周期性迭代,并且你也能遵循测试驱动开发的原则了。
(原文链接网址:http://blog.objectmentor.com/articles/category/testing-guis; Robert C. Martin的英文blog网址:http://blog.objectmentor.com/)
作者简介:Robert C. Martin是Object Mentor公司总裁,面向对象设计、模式、UML、敏捷方法学和极限编程领域内的资深顾问。他不仅是Jolt获奖图书《敏捷软件开发:原则、模式与实践》(中文版)(《敏捷软件开发》(英文影印版))的作者,还是畅销书Designing Object-Oriented C++ Applications Using the Booch Method的作者。Martin是Pattern Languages of Program Design 3和More C++ Gems的主编,并与James Newkirk合著了XP in Practice。他是国际程序员大会上著名的发言人,并在C++ Report杂志担任过4年的编辑。
The above is the detailed content of How to test JSP pages outside the container. For more information, please follow other related articles on the PHP Chinese website!