首頁  >  文章  >  Java  >  如何測試容器外的JSP頁面

如何測試容器外的JSP頁面

巴扎黑
巴扎黑原創
2017-08-10 15:12:491612瀏覽

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的呼叫出現在最後一個任務。

 

 

#

 

 

  <屬性名稱=“build.jar.home”值=“${build.home}/jars”/>

 

## 

## 

#   

#     

##   

   

    ;

##

   

   

     

   

   

   

<檔案集目錄=「${catalina.home}/shared/lib」>

     

<包含名稱=「*.jar」/ >

   

< /檔集>

###  ##### ############################################################################################################# #####  ############### ###    ##### #############    ########## ########    ###

   

     

     

     

       < ;fileset dir="${catalina.home}/common/lib">< ;fileset dir="${catalina.home}/common/lib">

############          #### ##############        ############        ###
######fileset>#### #############        ###################     ############ #####        ###################################################################################### ##### #############          ###### ############     ###### #########

       

#       

       

     

#     classpath>

   

##   

        

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);################  ###%>#################Ġ ####

 

">

   

   

   

 

## 

##   

}

## 

%>表>

}%>

#則下面是Jasper所產生的程式碼。

#package com.objectmentor.library.jsp.WEB_002dINF.pages.patrons.books ;

#導入javax.servlet.*;

導入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;

」公共最終類loanRecords_jsp擴展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 請求,HttpServletResponse 響應)

       

拋出java.io.IOException, ServletException {

##    JspFactory _jspxFactory = null;

#    PageContext pageContext = null;

###########################################################################################################################################nn\ ######    ###HttpSession session = null;################    ###ServletContext application = null;############ ##    ###ServletConfig config = null;###############    ###JspWriter out = null;## #############   ####################白o# #物件頁面= this;######

##    JspWriter _jspx_out = null;

##    PageContext _jspx_page_context = null;

    try {

      _jspxFactory = JspFactory.getDefaultFactory();y();

#      response.setContentType("text/html");

      pageContext = _jspxFactory.getPageContext (這PageContext ,請求,回應,

##                 

null, true, 8192, true); _jspx_page_context = pageContext;##     

application = pageContext.getServletContext();

# pageContext.getServletConfig();

##      session = pageContext.getSession();

# 。

      out.write('/n');

#      out.write('/n');

      out.write('/n');

 

List LoanRecords = (List) request.getAttribute("loanRecords");

##  if (loanRecords.size() > 0) {

      out.write("/n");

#     

out.write("
IDTitleDue date Fine

   

    td>

   

##   

   

/n");

#      out.write(" 

/n");

#      out.write( "#out.write("    

/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("

   

/n");

     

## out.write("

   

/n");

      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 實例並開始呼叫它!
<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>();<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年的编辑。

ID 很好
");       out.print(loanRecord.id);

#      out.write("/n");

##      out.write("   

");      out.print(loanRecord.title);

##      out.write("/n");

#      out.write("

");#      out.print(DateUtil.dateToString(loanRecord.dueDate));

##     

##      out .

     

out .    write("/n");

      out.write("   

");

#     

     

out.print(loanRecord.fine.toString());

#      out.write("/n");

      out.write("

   

以上是如何測試容器外的JSP頁面的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn