In-container 測試


在Java 中,容器(Container)的表面意涵,代表著一個Java寫的程式,實質上,容器抽象了環境的概念。JVM是Java 程式唯一認得的虛擬作業系統,容器則是運行於這個作業系統上的Java程式,代表著某個環境資源。例如,Web容器,代表 著運行於JVM虛擬作業系統上的虛擬HTTP伺服器,是Servlet/JSP唯一認識的HTTP伺服器。

那麼你要怎麼測試與容器互動的服務?例如,你寫了個Servlet:
package cc.openhome;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String user = req.getParameter("user").trim();
String passwd = req.getParameter("passwd").trim();
String page = "login.html";
if("justin".equals(user) && "1234".equals(passwd)) {
page = "success.html";
}
req.getRequestDispatcher(page).forward(req, resp);
}
}

那麼你要怎麼測試這個Servlet的運作?實際作好相關設定、部署(Deploy)至容器,然後開啟瀏覽器執行?這已經步入功能測試(Functional test)的範圍,而非單元測試,你不僅測試 了Servlet,連同部署設定是否正確等,都一併測試了。

可以嘗試以 Dummy 物件 的概念來進行測試。例如:
package test.cc.openhome;

import cc.openhome.LoginServlet;
import java.security.Principal;
import static org.junit.Assert.*;
import org.junit.*;
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

class TestForLoginServlet extends LoginServlet {
    public void doTest(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        doPost(req, resp);
    }
}

class DummyHttpServletRequest extends HttpServletRequestWrapper {
    private Map<String, String> parameters;
    private String forwardToPage;
    private boolean isForwarded;

    public DummyHttpServletRequest(Map<String, String> parameters) {
        super(new HttpServletRequest() {
            // 一些方法本體為空的實作
            // 純綷滿足HttpServletRequestWrapper建構的要求
        });
        this.parameters = parameters;
    }

    @Override
    public String getParameter(String name) {
        return parameters.get(name);
    }

    @Override
    public RequestDispatcher getRequestDispatcher(String path) {
        forwardToPage = path;
        return new RequestDispatcher() {
            public void forward(ServletRequest req, ServletResponse resp)
                    throws ServletException, IOException {
                isForwarded = true;
            }

            public void include(ServletRequest req, ServletResponse resp)
                    throws ServletException, IOException {
            }
        };
    }

    public String getForwardToPage() {
        return forwardToPage;
    }

    public boolean isForwarded() {
        return isForwarded;
    }
}

public class LoginServletTest {
    private TestForLoginServlet loginServlet;

    @Before
    public void setUp() {
        loginServlet = new TestForLoginServlet();
    }

    @Test
    public void testLoginSuccess() throws Throwable {
        Map<String, String> param = new HashMap<String, String>();
        param.put("user", "justin");
        param.put("passwd", "1234");
        DummyHttpServletRequest dummyRequest =
            new DummyHttpServletRequest(param);

        loginServlet.doTest(dummyRequest, null);
        assertTrue(dummyRequest.isForwarded());
        assertEquals("success.html", dummyRequest.getForwardToPage());
    }

    @Test
    public void testLoginFail() throws Throwable {
        Map<String, String> param = new HashMap<String, String>();
        param.put("user", "someone");
        param.put("passwd", "1234");
        DummyHttpServletRequest dummyRequest =
            new DummyHttpServletRequest(param);

        loginServlet.doTest(dummyRequest, null);
        assertTrue(dummyRequest.isForwarded());
        assertEquals("login.html", dummyRequest.getForwardToPage());
    }
}


最主要的是Servlet中,實際上需要的是從HttpServletRequest取 得請求參數,因此設計DummyHttpServletRequest讓Servlet的測試得以運行,就這個例子而言,自行設計 Dummy物件是有些麻煩,另一方式,就是使用Mock框架所提供的Mock物件,這之後會再談到。

然而事實上,HttpServletRequest的 實作是由容器提供,容器的行為實際上更為複雜,容器所管理的物件亦有其生命週期等議題,如果你要測試的,並 不是上面那個簡單的Servlet,那麼用Dummy物件或Mock物件,皆不足以代表實際容器所提供的物件,你所需要的,是從容器中獲取更貼近部署環境 的物件。

可以使用 Embedded 資源 的方式,實際運行一個嵌入式容器,想辦法從中獲取相關資源進行測試。例如:
package test.cc.openhome;

import static org.junit.Assert.*;
import java.io.IOException;
import java.net.URL;
import javax.servlet.*;
import javax.servlet.http.*;
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.junit.*;
import cc.openhome.LoginServlet;

class TestForLoginServlet extends LoginServlet {
public void doTest(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doPost(req, resp);
}
}

class DummyHttpServletRequest extends HttpServletRequestWrapper {
private String forwardToPage;
private boolean isForwarded;

public DummyHttpServletRequest(HttpServletRequest request) {
super(request);
}

@Override
public RequestDispatcher getRequestDispatcher(String path) {
forwardToPage = path;
return new RequestDispatcher() {
public void forward(ServletRequest req, ServletResponse resp)
throws ServletException, IOException {
isForwarded = true;
}

public void include(ServletRequest req, ServletResponse resp)
throws ServletException, IOException {
}
};
}

public String getForwardToPage() {
return forwardToPage;
}

public boolean isForwarded() {
return isForwarded;
}
}

public class LoginServletTest {
private Server server;
private DummyHttpServletRequest dummyRequest;

@Before
public void setUp() throws Exception {
server = new Server(8080);
server.setHandler(new AbstractHandler() {
public void handle(String target,
Request baseRequest,
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
dummyRequest = new DummyHttpServletRequest(request);
new TestForLoginServlet().doTest(dummyRequest, response);
baseRequest.setHandled(true);
}
});
server.start();
}

@After
public void tearDown() throws Exception {
server.stop();
}

@Test
public void testLoginSuccess() throws Throwable {
URL url = new URL("http://localhost:8080/?user=justin&passwd=1234");
url.openStream().read();
assertTrue(dummyRequest.isForwarded());
assertEquals("success.html", dummyRequest.getForwardToPage());
}

@Test
public void testLoginFail() throws Throwable {
URL url = new URL("http://localhost:8080/?user=someone&passwd=1234");
url.openStream().read();
assertTrue(dummyRequest.isForwarded());
assertEquals("login.html", dummyRequest.getForwardToPage());
}
}

在上面,請求會發送給內嵌的Jetty容器,Jetty容器產生HttpServletRequest、 HttpServletResponse,你將所需的HttpServletRequest包 裹為DummyHttpServletRequest物 件,再產生Servlet進行測試,並驗證測試結果。

這是容器內(In-container)測試的基本概念,你互動的資源或物件,是從實際的容器取得,這樣的測試,更貼近於整合測試(Integration test),因為你 所獲取的物件或資源更貼近於實際環境,可以得到更可靠的測試結果。

在容器內測試框架的部份,Servlet/JSP可以使用Cactus,JSF 可以使用JSFUnit(擴充了Cactus),OSGi則有JUnit4OSGi,在EJB測試的部份,若需要以嵌入式的方式來執行測試, 可以瞭解一下OpenEJB