如何测试容器外的JSP页面
Jsp测试技术
开发web应用程序最恼人的一点就是想要测试的话你就必须向将其部署好。当然,并不是所有部分都这样。如果你是经过了精心的设计的话,你可以在Java程序中测试业务逻辑。你可以在应用服务器不运行的情况下测试数据访问、接口以及存储过程。不过如果是测试GUI的话(由Jsp所产生的HTMl),你就必须向将其部署,然后才可能测试。
很多的团队求助于Sellenium,Mercury或是其他的一些工具通过web server来测试GUI。然而,即使是页面的内容不变但样式变了得情况也会让测试变得脆弱不堪。其他的团队使用Cactus解决这种脆弱性,或是用HtmlUnit、HttpUnit这样原始的工具来监测web应用程序所生成的HTML。对于这些问题,我会在另一系列的blog之中来谈论。
本文之中我会介绍一种简单易行的技术,它使用JUnit或是HtmlUnit来测试Jsp页面,并且完全脱离容器。这项技术的优势也在此。
你不必一定保持容器的运行,甚至存在。你可以在选择特定的webserver之前就测试你的Jsp。
你不必在每次修改后重新部署,因而编辑/编译/测试的过程会更迅速。
你可以使用测试优先开发的方式来持续的构建Jsp。
容器外测试Jsp技术之所以并不盛行是因为Jsp在设计上就运行于容器内的。设计者从未过多的想过容器外运行的可能。因此由Jsp编译器的所生成代码往往依赖于容器所提供的诸多组件。即使是生成Jsp代码的工具也假定了你已经有一个成功部署的web应用程序在运行。因此,为了在容器外运行,你就要开发出相应的这些工具和组件。
依赖管理的抱怨
为什么这么多框架和工具的设计者们总期望你生活在他们提供的狭小世界中?为什么我必须先构建出完整的web应用才能编译Jsp?为什么这些东西一定要运行在容器中?信息隐藏早在10年前就已经是优秀软件设计的基本信条了。我们这个行业何时才能认真对待它?
编译Jsp
测试Jsp的第一步是将其编译为servlet。实现这一步,我们还需要先将Jsp转换成Java格式。Apache提供了一个叫做Jasper的工具,我们调用Jasper为MyPage.jsp创建一个Java格式的源文件MyPage_jsp.java。然后,你就可以使用你最喜欢的IDE编译这个文件成Servlet。
可惜Jasper并非是设计用在命令行中使用的,或者说并不是完全这样设计的。但Jasper确有一个main函数用来处理命令行参数,而且通过调用java org.apache.jasper.JspC就能够轻易调用它了。不过,Jasper期望它所运行的环境与容器环境是保持一致的。你要确保classpath中有了很多apache的Jar文件,而且它要能找到web应用程序的web.xml。它还需要能够找到包含web应用程序Jar以及TLD文件等的WEB-INF目录。简而言之,Jasper需要能找到一个完整的web应用程序。
如果事情更糟的话,除非是与TOMCAT的调用方式保持完全一致,否则某些特定的Jasper版本(我用的是tomcat 5.5.20)存在一些bug,它生成的代码会有一些错误。
第一点要做的虽然繁琐但还算简单,你需要创建好正确的目录以及文件结构,然后在Ant(Classpath更容易控制)中调用Jasper。第二点就需要一定的研究和测试才能让它跑起来。以下就是能成功运行的ant文件。JspC的调用出现在最后一个任务中。
includes="**/jsp/**/*.class"
/>
当然,你要让所有标准文件以及目录都在${build.war.home}之下以确保工作。如果你在你的Jsp之中使用了自定义tag的话,还要确保所有相应的TLD文件都在你的TLD目录之中。
要注意的是,在ant文件中调用Jspc的命令行,而不是使用Tomcat所提供的JspC的Ant Task。因为我发现当你有自定义tag的时候它无法正确运行。也许我犯了糊涂,或者JspC中确实有bug。不过我所发现的唯一能让Jasper生成正确代码的方式是从命令行调用它,并明确的传递Jsp文件路径作为命令行的参数!如果你依靠它的Ant Task或是使用命令行来搜索所有web应用中的Jsp进行编译的话,它就会生成错误的代码。(请参阅这篇blog)
现在我们有了Java文件,让我们来分析一下它。首先,请看下面的Jsp文件。
<%@ 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.print(loanRecord.id); out.write("/n"); out.write(" out.write(" out.print(loanRecord.title); out.write("/n"); out.write(" out.write(" out.print(DateUtil.dateToString(loanRecord.dueDate)); out.write("/n"); out.write(" out.write(" out.print(loanRecord.fine.toString()); out.write("/n"); out.write(" out.write(" out.write(" ");
}
out.write("/n"); out.write("/n");
/n");/n");
/n");ID /n");
Title /n");
Due date /n");
Fine /n");
/n");");
/n");
");
/n");
");
/n");
");
/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);
}
} finally {
if (_jspxFactory != null) _jspxFactory.releasePageContext(_jspx_page_context);
}
}
}
最后的抱怨
这个类为什么要声明为final呢?如果我想创建一个测试的stub派生类呢?为什么有人会觉得生成类如此不可冒犯以至于我都无法覆写它。
仔细读过这段代码你就会发现,要想使用这个servlet的实例我们需要HttpServletRequest以及HttpServletResponse的实例。
更仔细研读一下我们就会发现servlet将所有的HTML写到JspWriter的实例中,而JspWriter是从PageContext中获得的。如果我们能够创建一个JspWriter的mock up的版本来保存所有的这些HTML,再为PageContext创建一个mock up的版本来派送mock JspWriter,那么我们就能在我们的测试中访问这些HTML了。
幸运的是,Tomcat的设计人员把JspWriter的创建放入到了JspFactory的工厂类中。而这个工厂类是可以覆写的!这就意味着我们可以在servlet之中获得我们自己的JspWriter类而不用改变servlet。需要的就是下面这段代码。
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;
}
}
现在,我们需要的是mock Jspwriter。为了便于展示,我用了下面的:
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
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. {})
}
无需关心那些我省略掉的未实现方法,我认为只需要关心那些足够使得我的测试得以运行的方法即可。对于剩下的,我只会使用其退化实现。
我的IDE对于创建这些mock类非常有帮助。它能够自动化的构建方法原型,并为那些接口或是抽象类所需要实现的方法给出退化的实现。
同样的用类似方法创建出MockPageContext,MockHttpServletRequest以及MockHttpServletResponse类。
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实例并且开始调用它!我的头一个测试用例就像下面这样:
<br/>
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();
<br/>
jspPage._jspInit();
jspPage._jspService(request, response);
<br/>
assertEquals("", jspWriter.getContent());
}
<br/>
就像预期的一样,测试失败了。这是因为还有些内容还没补充上,不过所剩无多。如果你仔细的看过Jsp文件,你就会发现它调用了request.getAttribute(“loanRecords”)并且期望返回一个List。但因为目前的测试并未为这样的属性赋值,从而导致了代码抛出了异常。
要想成功让servlet输出HTML,我们还需要加载这个属性。然后,我们就可以使用HtmlUnit来解析此HTML并且编写相应的单元测试。
HtmlUnit非常的容易使用,尤其是在测试所产生的像是本例这样的web pages上。我这里还有篇文章详细的介绍了它。
下面就是最终测试加载属性的测试,它通过htmlunit来检测HTML,并且做出正确的判断:
<br/>
package com.objectmentor.library.jspTest.books.patrons.books;
<br/>
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;
<br/>
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.jsp.*;
import java.util.*;
<br/>
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;
<br/>
protected void setUp() throws Exception {
jspWriter = new MockJspWriter();
pageContext = new MockPageContext(jspWriter);
mockFactory = new MockJspFactory(pageContext);
<br/>
JspFactory.setDefaultFactory(mockFactory);
response = new MockHttpServletResponse();
request = new MockHttpServletRequest();
webClient = new WebClient();
webClient.setJavaScriptEnabled(false);
dummyWindow = new TopLevelWindow("", webClient);
}
<br/>
public void testLoanRecordsPageGeneratesAppropriateTableRows() throws Exception {
HttpJspBase jspPage = new loanRecords_jsp();
jspPage._jspInit();
<br/>
List<LoanRecord> loanRecords = new ArrayList<LoanRecord>();<div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false"> addLoanRecord(loanRecords,
"99",
"Empire",
DateUtil.dateFromString("2/11/2007"),
new Money(4200));
addLoanRecord(loanRecords,
"98",
"Orbitsville",
DateUtil.dateFromString("2/12/2007"),
new Money(5200));
<br>
request.setAttribute("loanRecords", loanRecords);
<br>
jspPage._jspService(request, response);
<br>
StringWebResponse stringWebResponse = new StringWebResponse(jspWriter.getContent());
HtmlPage page = HTMLParser.parse(stringWebResponse, dummyWindow);
HtmlElement html = page.getDocumentElement();
<br>
HtmlTable table = (HtmlTable) html.getHtmlElementById("loanRecords");
List<HtmlTableRow> rows = table.getHtmlElementsByTagName("tr");
assertEquals(3, rows.size());
<br>
assertEquals("even", classOfElement(rows.get(1)));
assertEquals("odd", classOfElement(rows.get(2)));
<br>
List<HtmlTableDataCell> firstRowCells = rows.get(1).getCells();
assertEquals(4, firstRowCells.size());
<br>
List<HtmlTableDataCell> secondRowCells = rows.get(2).getCells();
assertEquals(4, secondRowCells.size());
<br>
assertLoanRecordRowEquals("99", "Empire", "02/11/2007", "$42.00", firstRowCells);
assertLoanRecordRowEquals("98", "Orbitsville", "02/12/2007", "$52.00", secondRowCells);
}
<br>
private String classOfElement(HtmlTableRow firstDataRow) {return firstDataRow.getAttributeValue("class");}
<br>
private void assertLoanRecordRowEquals(String id, String title, String dueDate, String fine, List<HtmlTableDataCell> 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());
}
<br>
private void addLoanRecord(List<LoanRecord> 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;
<br>
loanRecords.add(loanRecord);
}
<br>
private class MockJspFactory extends JspFactory {
private PageContext pageContext;
public MockJspFactory(PageContext pageContext) {
this.pageContext = pageContext;
}
<br>
public PageContext getPageContext(Servlet servlet, ServletRequest servletRequest, ServletResponse servletResponse, String string, boolean b, int i, boolean b1) {
return pageContext;
}
<br>
public void releasePageContext(PageContext pageContext) {
}
<br>
public JspEngineInfo getEngineInfo() {
return null;
}
}
}
<br>
<span style="font-size: 9pt;">上述的测试确保了所生成的HTML中表格中的每一行都具有正确的内容。这项测试确实能够测出是否存在这样的表格,并且判断出是否表格的每一行是按照正确的顺序来展现的。同时,它也确保了每一行的相应style。测试忽略了此外的表单以及语法部分。</span>
结论
这篇发表在此的技术能够用来测试几乎所有目前我们所见过的web页面,并且脱离容器,也无需web server的运行。相对来说,它也比较容易去设置,并且非常易于扩展。有了它,你就可以快速的进行编辑、编译、测试的周期性迭代,并且你也能遵循测试驱动开发的原则了。<br>
(原文链接网址: 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年的编辑。
以上是如何测试容器外的JSP页面的详细内容。更多信息请关注PHP中文网其他相关文章!

热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

AI Hentai Generator
免费生成ai无尽的。

热门文章

热工具

记事本++7.3.1
好用且免费的代码编辑器

SublimeText3汉化版
中文版,非常好用

禅工作室 13.0.1
功能强大的PHP集成开发环境

Dreamweaver CS6
视觉化网页开发工具

SublimeText3 Mac版
神级代码编辑软件(SublimeText3)

热门话题

是否要复制MicrosoftWord中的页面,并保持格式不变?这是一个聪明的想法,因为当您想要创建特定文档布局或格式的多个副本时,在Word中复制页面可能是一种有用的节省时间的技术。本指南将逐步引导您在Word中复制页面的过程,无论是创建模板还是复制文档中的特定页面。这些简单的说明旨在帮助您轻松地重新制作页面,省去从头开始的麻烦。为什么要在MicrosoftWord中复制页面?在Word中复制页面非常有益的原因有以下几点:当您有一个具有特定布局或格式的文档要复制时。与从头开始重新创建整个页面不同

如何使用WebSocket和JavaScript实现在线语音识别系统引言:随着科技的不断发展,语音识别技术已经成为了人工智能领域的重要组成部分。而基于WebSocket和JavaScript实现的在线语音识别系统,具备了低延迟、实时性和跨平台的特点,成为了一种被广泛应用的解决方案。本文将介绍如何使用WebSocket和JavaScript来实现在线语音识别系

WebSocket与JavaScript:实现实时监控系统的关键技术引言:随着互联网技术的快速发展,实时监控系统在各个领域中得到了广泛的应用。而实现实时监控的关键技术之一就是WebSocket与JavaScript的结合使用。本文将介绍WebSocket与JavaScript在实时监控系统中的应用,并给出代码示例,详细解释其实现原理。一、WebSocket技

页面刷新在我们日常的网络使用中非常常见,当我们访问一个网页后,有时候会遇到一些问题,比如网页加载不出来或者显示不正常等。这时候我们通常会选择刷新页面来解决问题,那么如何快速地刷新页面呢?下面我们就来探讨一下页面刷新的快捷键。页面刷新快捷键是一种通过键盘操作来快速刷新当前网页的方法。在不同的操作系统和浏览器中,页面刷新的快捷键可能有所不同。下面我们以常见的W

华为官方消息显示,开放原子开发者大会以“一切为了开发者”为主题,在无锡举办了两天,时间为12月16日至17日会上,由开放原子开源基金会主导,华为、浪潮、DaoCloud、谐云、青云、飓风引擎以及OpenSDV开源联盟、openEuler社区、OpenCloudOS社区等成员单位共同发起建设的AtomHub可信镜像中心正式开放公测。AtomHub秉承共建、共治、共享的理念,旨在为开源组织和开发者提供中立、开放共建的可信开源容器镜像中心。鉴于DockerHub等镜像仓库的不稳定性和不可控性,以及一些

标题:3秒跳转页面实现方法:PHP编程指南在网页开发中,页面跳转是常见的操作,一般情况下我们使用HTML中的meta标签或者JavaScript的方法进行页面跳转。不过,在某些特定的情况下,我们需要在服务器端进行页面跳转。本文将介绍如何使用PHP编程实现一个在3秒内自动跳转到指定页面的功能,同时会给出具体的代码示例。PHP实现页面跳转的基本原理PHP是一种在

《处理Laravel页面无法正确显示CSS的方法,需要具体代码示例》在使用Laravel框架开发Web应用时,有时候会遇到页面无法正确显示CSS样式的问题,这可能会导致页面呈现不正常的样式,影响用户体验。本文将介绍一些处理Laravel页面无法正确显示CSS的方法,并提供具体的代码示例,帮助开发者解决这一常见问题。一、检查文件路径首先要检查CSS文件的路径是

JavaScript和WebSocket:打造高效的实时天气预报系统引言:如今,天气预报的准确性对于日常生活以及决策制定具有重要意义。随着技术的发展,我们可以通过实时获取天气数据来提供更准确可靠的天气预报。在本文中,我们将学习如何使用JavaScript和WebSocket技术,来构建一个高效的实时天气预报系统。本文将通过具体的代码示例来展示实现的过程。We
