>  기사  >  Java  >  컨테이너 외부에서 JSP 페이지를 테스트하는 방법

컨테이너 외부에서 JSP 페이지를 테스트하는 방법

巴扎黑
巴扎黑원래의
2017-08-10 15:12:491558검색

Jsp테스트 기술

웹 애플리케이션 개발에서 가장 짜증나는 점은 테스트하려면 배포해야 한다는 것입니다. 물론 모든 부분이 이렇지는 않습니다. 신중하게 디자인하면 Java 프로그램에서 비즈니스 로직을 테스트할 수 있습니다. 애플리케이션 서버를 실행하지 않고도 데이터 액세스, 인터페이스 및 저장 프로시저를 테스트할 수 있습니다. 그러나 GUI(Jsp에서 생성된 HTML)를 테스트하는 경우 테스트하기 전에 먼저 배포해야 합니다.

많은 팀이 웹 서버를 통해 GUI를 테스트하기 위해 Sellenium, Mercury 또는 기타 도구를 사용합니다. 그러나 페이지의 내용은 그대로 유지하고 스타일이 변경되더라도 테스트는 취약해집니다. 다른 팀에서는 Cactus를 사용하여 이 취약점을 해결하거나 HtmlUnit 및 HttpUnit과 같은 기본 도구를 사용하여 웹 애플리케이션에서 생성된 HTML을 모니터링합니다. 이러한 문제는 다른 블로그 시리즈에서 논의하겠습니다.

이 기사에서는 JUnit이나 HtmlUnit을 사용하여 Jsp 페이지를 테스트하고 컨테이너와 완전히 분리되는 간단하고 쉬운 기술을 소개하겠습니다. 이는 이 기술의 장점이기도 합니다.

컨테이너를 계속 실행하거나 기존 상태로 유지할 필요는 없습니다. 특정 웹서버를 선택하기 전에 Jsp를 테스트할 수 있습니다.

수정할 때마다 다시 배포할 필요가 없으므로 편집/컴파일/테스트 프로세스가 더 빨라집니다.

테스트 우선 개발을 사용하여 지속적으로 Jsp를 빌드할 수 있습니다.

JSP 기술이 컨테이너 외부에서 인기가 없는 이유는 JSP가 컨테이너 내부에서 실행되도록 설계되었기 때문입니다. 디자이너들은 컨테이너 외부에서 실행될 가능성에 대해 많이 생각해 본 적이 없습니다. 따라서 Jsp 컴파일러에서 생성된 코드는 컨테이너에서 제공하는 많은 구성 요소에 의존하는 경우가 많습니다. JSP 코드를 생성하는 도구라도 이미 성공적으로 배포된 웹 애플리케이션이 실행되고 있다고 가정합니다. 따라서 컨테이너 외부에서 실행하려면 이러한 도구와 구성 요소를 개발해야 합니다.

종속성 관리 불만 사항

왜 그렇게 많은 프레임워크와 도구의 설계자들은 여러분이 자신들이 제공하는 작은 세상에서 살기를 기대합니까? JSP를 컴파일하기 전에 완전한 웹 애플리케이션을 구축해야 하는 이유는 무엇입니까? 왜 이런 것들이 컨테이너에서 실행되어야 합니까? 정보 은닉은 이미 10년 전부터 좋은 소프트웨어 설계의 기본 원칙이었습니다. 우리 업계는 언제 이것을 심각하게 받아들일까요?

Jsp 컴파일

Jsp 테스트의 첫 번째 단계는 이를 서블릿으로 컴파일하는 것입니다. 이 단계를 달성하려면 먼저 Jsp를 Java 형식으로 변환해야 합니다. Apache는 Jasper라는 도구를 제공하여 MyPage.jsp에 대한 Java 형식 소스 파일 MyPage_jsp.java를 생성합니다. 그런 다음 선호하는 IDE를 사용하여 이 파일을 서블릿으로 컴파일할 수 있습니다.

안타깝게도 Jasper는 명령줄에서 사용하도록 설계되지 않았거나 정확히 그런 식으로 설계되지 않았습니다. 그러나 Jasper에는 명령줄 매개변수를 처리하는 기본 기능이 있으며 java org.apache.jasper.JspC를 호출하여 쉽게 호출할 수 있습니다. 그러나 Jasper는 실행되는 환경이 컨테이너 환경과 일치할 것으로 기대합니다. 클래스 경로에 Apache Jar 파일이 많이 있는지, 그리고 웹 애플리케이션의 web.xml을 찾을 수 있는지 확인해야 합니다. 또한 웹 애플리케이션 Jar와 TLD 파일 등이 포함된 WEB-INF 디렉터리를 찾을 수 있어야 합니다. 즉, Jasper는 완전한 웹 애플리케이션을 찾을 수 있어야 합니다.

상황이 더 나쁘다면 TOMCAT이 호출되는 방식과 완전히 일치하지 않는 한 일부 특정 Jasper 버전에 버그가 있고(저는 tomcat 5.5.20을 사용하고 있습니다) 이것이 생성하는 코드에 약간의 오류가 있을 것입니다.

먼저 해야 할 일은 지루하지만 비교적 간단합니다. 올바른 디렉터리와 파일 구조를 만든 다음 Ant에서 Jasper를 호출해야 합니다(Classpath가 제어하기 더 쉽습니다). 두 번째 요점은 그것이 작동하려면 약간의 연구와 테스트가 필요하다는 것입니다. 다음은 성공적으로 실행될 수 있는 ant 파일이다. JspC에 대한 호출은 마지막 작업에 나타납니다.

<프로젝트 이름="라이브러리" default="compile" basedir=".">

<속성 환경="env"/>

 

 

 

 

 

 

 

   

     

  

   

   

     

   

   

     

   

    < ;경로요소 위치=" ${catalina.home}/shared/classes"/>

   

     

   

 

& lt;대상 이름="clean">

   

   

 

 

   

   

      < ;classpath refid="compile.classpath"/>

   

 

 

   

    < ;jar jarfile="${build.jar.home}/application.jar" basedir="${build.classes.home}" include="**/application/**/*.class" />

 

 

   

     

   

   

     

   

    

    < ;jar jarfile="${dist.home}/${app.name}.war" basedir="${build.war.home}"/>

 

ㅋㅋㅋ

     

     

     

       

         

       

       

          <이름 포함= "*.jar"/>

       

       

          < include name="*.jar"/>

        

ㅋㅋㅋ

includes= "**/jsp/**/*.class"

                                                   

물론 ${build.war.home 아래에 모든 표준 파일과 디렉터리가 있어야 합니다. } 작동하는지 확인하세요. Jsp에서 사용자 정의 태그를 사용하는 경우 해당 TLD 파일이 모두 TLD 디렉토리에 있는지 확인하십시오.

Tomcat에서 제공하는 JspC Ant Task를 사용하는 대신 Jspc의 명령줄을 ant 파일에서 호출한다는 점에 유의해야 합니다. 맞춤 태그가 있으면 제대로 작동하지 않는다는 것을 알았기 때문입니다. 아마도 제가 혼란스럽거나 실제로 JspC에 버그가 있을 수도 있습니다. 하지만 Jasper가 올바른 코드를 생성하도록 하는 유일한 방법은 명령줄에서 Jasper를 호출하고 명시적으로 Jsp 파일 경로를 명령줄 인수로 전달하는 것입니다! Ant 작업에 의존하거나 명령줄을 사용하여 컴파일을 위해 모든 웹 애플리케이션에서 Jsps를 검색하면 잘못된 코드가 생성됩니다. (이 블로그 참조)

이제 Java 파일이 있으므로 분석해 보겠습니다. 먼저 아래 Jsp 파일을 살펴보시기 바랍니다.

<%@ 페이지 가져오기="com.objectmentor.library.utils.DateUtil" %><%@ 페이지 가져오기="com.objectmentor.library.web. Controller.patrons.LoanRecord" %>

<%@ page import="java.util.List" %>

<%

List LoanRecords = (목록) 요청 .getAttribute("loanRecords");

if (loanRecords.size() > 0) {

%>

lt;/th>

<일>마감일

일>괜찮습니다

<%

for (int i = 0; i < LoanRecords.size(); i++) {

                 ~    

 

">

   

   

   

 

  <%

    }

  %>

<%=loanRecord. id%>

   

<%=loanRecord.title%>

    & lt;/td>

   

<%=DateUtil.dateToString(loanRecord.dueDate)%>

   

& lt;%=loanRecord.fine. toString()%>

   

<%

  }

%>

하면则是Jasper所生成的代码。

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

import 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;

public 최종 클래스 LoanRecords_jsp는 org.apache.jasper.runtime.HttpJspBase

을 확장합니다.    implements org. 아파치.재스퍼.런타임. JspSourceDependent {

  private static java.util.List _jspx_dependents;

  public Object getDependants() {

    return _jspx_dependents;

  }

  public void _jspService(HttpServletRequest request, HttpServletResponse response)

        java.io. IOException, ServletException {

    JspFactory _jspxFactory = null;

    PageContext pageContext = null;

    HttpSession 세션 = null;

    ServletContext 애플리케이션 = null;

    ServletConfig 구성 = null;

    JspWriter out = null;

    개체 페이지 = this;

    JspWriter _jspx_out = null;

    PageContext _jspx_page_context = null;

    시도해 보세요

      _jspxFactory = JspFactory.getDefaultFactory();

      response.setContentType("text/html");

      pageContext = _jspxFactory.getPageContext(this, 요청, 응답,

                null, 참, 8192, 참);

      _jspx_page_context = pageContext;

      application = pageContext.getServletContext();

      config = pageContext.getServletConfig();

      session = pageContext.getSession();

      out = pageContext.getOut();

      _jspx_out = out;

      out.write('/ n');

      아웃. write('/n');

      out.write('/n');

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

  if (loanRecords.size() > 0) {

      out.write("/n");

     out.write("< 테이블 클래스=/"list/" id=/"loanRecords/">/n");

      out.write("  /n");

      out.write("    번째>ID/n");

      out.write("    번째>제목/n");

      out.write("    <일>마감일/n");

      out.write("    <일>괜찮습니다/n ");

      out.write("  /n");

      out.write("  ");

    for (int i = 0; 나는 < 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("    /n");

      out.write ("    ");

      out.print(loanRecord.title);

      out.write("/n");

      아웃. write("    /n");

      out.write("    ");

      out.print(날짜 Util.dateToString(loanRecord .dueDate));

      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") ;

      아웃. write("/n");

  }

    } catch(투척 가능 t) {

      if(! (t 인스턴스of 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 );

}

}

}

최종 불만

이 수업을 최종으로 선언해야 하는 이유는 무엇인가요? 테스트 스텁 파생 클래스를 생성하려면 어떻게 해야 합니까? 생성된 클래스가 너무 공격적이어서 재정의조차 할 수 없는 이유는 무엇입니까?

이 코드를 주의 깊게 읽으면 이 서블릿 인스턴스를 사용하려면 HttpServletRequest 및 HttpServletResponse 인스턴스가 필요하다는 것을 알게 될 것입니다.

좀 더 자세히 살펴보면 서블릿이 모든 HTML을 JspWriter 인스턴스에 쓰고 JspWriter가 PageContext에서 얻어지는 것을 알 수 있습니다. 이 모든 HTML을 담기 위해 JspWriter의 모형 버전을 생성한 다음, 모형 JspWriter를 전달하기 위해 PageContext의 모형 버전을 생성할 수 있다면 테스트에서 이 HTML에 액세스할 수 있을 것입니다.

다행히 Tomcat의 디자이너들은 JspWriter의 생성을 JspFactory의 팩토리 클래스에 넣었습니다. 그리고 이 팩토리 클래스는 재정의될 수 있습니다! 이는 서블릿을 변경하지 않고도 서블릿에서 자체 JspWriter 클래스를 얻을 수 있음을 의미합니다. 필요한 것은 다음 코드뿐입니다.

class MockJspFactory는 JspFactory를 확장합니다. servletResponse, 문자열 문자열, 부울 b, int i, 부울 b1) {

return new MockPageContext(new MockJspWriter());

}

public void releasePageContext(PageContext pageContext) {

}

public JspEngineInfo getEngineInfo() {

return null;

}

}

이제 우리는 모의 Jspwriter가 필요합니다. 프레젠테이션의 편의를 위해 다음을 사용했습니다.

MockJspWriter

package com.objectmentor.library.web.framework.mocks;

javax.servlet을 가져옵니다. jsp.JspWriter;

import java.io.IOException;

public 클래스 MockJspWriter는 JspWriter {

을 확장합니다 private StringBuffer submitContent;

public MockJspWriter(int bufferSize, boolean autoFlush) {

super(bufferSize, autoFlush);

submittedContent = new StringBuffer();

}

  public String getContent() {

    return submitContent.toString();

  }

  public void print(String arg0)에서 IOException이 발생합니다.

    submittedContent.append(arg0);

  }

  public void write(char[] arg0 , int arg1, int arg2) IOException이 발생합니다. {

    for (int i=0; i

      submittedContent.append(String.valueOf(arg0[arg1++]));

  }

  public void write(String content)가 IOException을 발생시킵니다. {

    submittedContent.append(content);

  }

  // 흥미롭지 않은 내용이 많음 메서드는 생략되었습니다.  방금

  // 구현을 저하시켰습니다.  (예: {})

}

无需关心那些我省略掉的未实现이 방법은 내 마음에 드는 测试得以运行적 방법입니다. 나의 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 클래스 MockPageContext는 PageContext를 확장합니다.

 

비공개 최종 JspWriter 출력;

 

비공개 HttpServletRequest 요청;

공개 MockPageContext(JspWriter out) {

   

this.out = out;

   

request = new MockHttpServletRequest();

 

}

 

public JspWriter getOut()

   

반품;

 

}

 

public ServletRequest getRequest() {

   

반품 요청;

  }

  // 많은 퇴화 기능이 제거되었습니다.

}

MockHttpServletRequest

패키지 com.objectmentor.library.web.framework.mocks;

import javax.servlet.*;

import javax.servlet.http.*;

import java.io.*;

import java.security.Principal;

import java.util.*;

public 클래스 MockHttpServletRequest는 HttpServletRequest를 구현합니다. {

private String 메서드

  개인 문자열 contextPath;

  private String requestURI;

  private HttpSession session = new MockHttpSession();

  private Map 매개변수 = new HashMap();

  비공개 지도 속성 = 신규 HashMap();

  public MockHttpServletRequest(String method, String contextPath,

                            문자열 요청URI ) {

    super();

    이것 .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( ) {

    return requestURI;

  }

  public String getServletPath() {

    return requestURI.substring(getContextPath().length ());

  }

  public HttpSession getSession() {

    반환 세션;

  }

  public HttpSession getSession (boolean arg0) {

    return session;

  }

  public Object getAttribute(String arg0) {

    return attribute.get( arg0);

  }

  public String getParameter(String arg0) {

    return (문자열) 매개변수.get(arg0);

  }

  public Map getParameterMap() {

    반환 매개변수;

 }

  공개 열거 getParameterNames( ) {

    return null;

  }

  public void setSession(HttpSession 세션)

    this.session = 세션;

  }

  public void setParameter(String s, String s1) {

    parameters.put(s, s1);

  }

  public void setAttribute(문자열 이름, 개체 값) {

    attributes.put(이름, 값);

  }

// 많은 퇴화 메서드가 제거되었습니다.

}

MockHttpServletResponse

package com.objectmentor.library.web.framework.mocks;

javax 가져오기 .Servlet.servletoutputStream;

import javax.servlet.http.*;

import java.io.*;

import java.util.locale;

홍보 클래스 MockHttpServletResponse는 HttpServletResponse를 구현합니다. ,现에서 나는 loanRecords_jsp의 서블릿을 사용했습니다.并且开始调사용它!저의 头一个测试用例就image下면这样:

<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年的编辑。

위 내용은 컨테이너 외부에서 JSP 페이지를 테스트하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.