/n");
# out.write(" /n");
# out.write( "#out.write("
ID | /n"); out.write("
標題/n"); out.write("#
#/n ");## out.write("
很好 | /n") ;## out.write("#
/n");
out.write(" ");
##
for (int i = 0; i # LoanRecord LoanRecord = (LoanRecord) LoanRecords.get(i );
##############。 ###################### ###out.write("/n");###### out.write("#
#
#
#
#out.print(i%2==0?"偶":"奇");#
out.write("/">/n " );#
out.write("
"); out.print(loanRecord.id); # out.write("/n");
## out.write("
| /n");
## out.write("
"); out.print(loanRecord.title); ## out.write("/n");
# out.write(" | /n");
out.write("
");# out.print(DateUtil.dateToString(loanRecord.dueDate));
##
## out . out . write("/n");
out.write(" | /n"); ## out.write("
"); # out.print(loanRecord.fine.toString());
# out.write("/n"); out.write("
| /n"); out.write ("
/n");
# out.write("
");
# }
out.write("/n");
out.write("表>/n" );
}
###} catch (Throwable t) {############## ###if (!(t instanceof SkipPageException) )){# ############## ####out = _jspx_out;############### ###if ( ) != 0)############## ###out.clearBuffer();################ != 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) {
}
#of
# 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;######## }
## // 省略了許多簡併函數。 }
MockHttpServletRequest
#MockHttpServletRequest
套件com.objectmentor.library.web.framework。 ;
匯入javax.servlet.*;
匯入javax.servlet。 .*;
導入java.io.*;
#導入java.security.Principal;
##import java.util.*;
#公共類別MockHttpServletRequest 實作HttpServletRequest {
## 私有字串方法;
# 私有字串contextPath;
private String requestURI;##
private HttpSession session = new MockHttpSession();## 私有對應參數= new HashMap();
# 私人對應屬性= new HashMap();
## public MockHttpServletRequest(String method, String contextPath,
# ,String##== this.method = 方法;
this.contextPath = contextPath;
## this .requestURI = requestURI;
## }
# # public MockHttpServletRequest() {
this("GET");
## }
public MockHttpServletRequest(字串方法){
this(方法, "/Library", "/Library/foo/bar.jsp");
# }
# public String getContextPath() {
## return contextPath;
## }
public String getMethod() {
############# ###回傳方法;######## ######### ###} ############################ ###public String getRequestURI() {# ############# # ###返回請求URI;############## ###}############################################################### ############# ### ###public String getServletPath() {############### ###return requestURI.substring(getContextPath( ).length());######### }
## public HttpSession getSession() {
回傳會話;
}
##
public HttpSession getSession(boolean arg0) {
# return session;
}
public Object getAttribute (字串arg0) {
return attribute.get(arg0);#
}
public String getParameter(String arg0) {
#
return(字串)parameters.get(arg0);
## }
public Map getParameterMap() {
##
回傳參數;## }
# 公開枚舉getParameterNames() {
## return null;
# }
public void setSession(HttpSession session) {
## this.session = 會話;
# }
## public void setParameter(String s, String s1) {
#parameters.put(s, s1);
## }
#public void setAttribute(字串名稱, 物件值) {
## attributes.put(name, 值);
##
}# ########################### ###// 省略了許多簡併方法。 ############} #########################MockHttpServletResponse############# package com.objectmentor.library.web.framework.mocks; ########################導入javax.servlet.ServletOutputStream;####### ######導入javax.servlet.http。 .Locale;########## ##############public class MockHttpServletResponse Implements HttpServletResponse {############## ##### ##### #// 所有功能實現為退化。 #有了這些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._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.*;
登入後複製
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();
登入後複製
登入後複製
List<LoanRecord> loanRecords = new ArrayList<LoanRecord>();<div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false"> addLoanRecord(loanRecords,
登入後複製
登入後複製
DateUtil.dateFromString("2/11/2007"),
登入後複製
addLoanRecord(loanRecords,
登入後複製
DateUtil.dateFromString("2/12/2007"),
登入後複製
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<HtmlTableRow> rows = table.getHtmlElementsByTagName("tr");
登入後複製
assertEquals(3, rows.size());
登入後複製
assertEquals("even", classOfElement(rows.get(1)));
登入後複製
assertEquals("odd", classOfElement(rows.get(2)));
登入後複製
List<HtmlTableDataCell> firstRowCells = rows.get(1).getCells();
登入後複製
assertEquals(4, firstRowCells.size());
登入後複製
List<HtmlTableDataCell> 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<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());
登入後複製
private void addLoanRecord(List<LoanRecord> loanRecords, String id, String title, Date dueDate, Money fine) {
登入後複製
LoanRecord loanRecord = new LoanRecord();
登入後複製
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) {
登入後複製
public void releasePageContext(PageContext pageContext) {
登入後複製
public JspEngineInfo getEngineInfo() {
登入後複製
<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年的编辑。