JUnit + Mockito 单元测试(三)

简介: 这里假设我们没有 Tomcat(虽然不太可能,假设吧!),那就使用 Mockito 模拟一个看看怎么样。本文结合 RESTful 接口来进行回归测试的目的。 模拟 ServletContextListener Listener 是启动 App 的第一个模块,相当于执行整个 Web 项目的初始化工作,所以也必须先模拟 ServletContextListener 对象。

这里假设我们没有 Tomcat(虽然不太可能,假设吧!),那就使用 Mockito 模拟一个看看怎么样。本文结合 RESTful 接口来进行回归测试的目的。

模拟 ServletContextListener

Listener 是启动 App 的第一个模块,相当于执行整个 Web 项目的初始化工作,所以也必须先模拟 ServletContextListener 对象。通过初始化的工作是安排好项目的相关配置工作和先缓存一些底层的类(作为 static 成员保存在内存中)。

package ajaxjs.test;

import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.*;
import java.io.IOException;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletException;
import ajaxjs.config.Application;
import org.junit.Before;
import org.junit.Test;
import ajaxjs.Constant;

public class TestApplication {
	private Application app;
	private ServletContext sc;
	
	@Before
	public void setUp() throws Exception {
		sc = mock(ServletContext.class);
		// 指定类似 Tomcat 的虚拟目录,若设置为 "" 表示 Root 根目录
		when(sc.getContextPath()).thenReturn("/zjtv"); 
		// 设置项目真实的目录,当前是 返回 一个特定的 目录,你可以不执行该步
		when(sc.getRealPath(anyString())).thenReturn("C:\\project\\zjtv\\WebContent" + Constant.ServerSide_JS_folder);
		// 设置 /META-INF 目录,当前使用该目录来保存 配置
		when(sc.getRealPath("/META-INF")).thenReturn("C:\\project\\zjtv\\WebContent\\META-INF");
		
		 app = new Application(); 
       }
	
	@Test
	public void testContextInitialized() throws IOException, ServletException {
		ServletContextEvent sce = mock(ServletContextEvent.class);
		when(sce.getServletContext()).thenReturn(sc);
		app.contextInitialized(sce);
		assertNotNull(sce);
		assertTrue("App started OK!", Application.isConfig_Ready);
 	}
}

上述代码中 Application app 是 javax.servlet.ServletContextListener 的实现。你可通过修改 setUp() 里面的相关配置,应适应你的测试。

模拟 Servlet

背景简介:由于这是 JSON RESTful 接口的原因,所以我使用同一个 Servlet 来处理,即 BaseServlet,为 HttpServlet 的子类,而且采用 Servlet 3.0 的注解方式定义 URL Mapping,而非配置 web.xml 的方式,代码组织更紧凑。——从而形成针对最终业务的 zjtvServlet 类,为 BaseServlet 的子类,如下,

package zjtv;

import javax.servlet.annotation.WebServlet;
import javax.servlet.annotation.WebInitParam;
import ajaxjs.service.BaseServlet;

@WebServlet(
	urlPatterns = {"/service/*", "/admin_service/*"},
	initParams = {
		@WebInitParam (name = "news", 		value = "ajaxjs.data.service.News"),
		@WebInitParam (name = "img", 		value = "ajaxjs.data.service.subObject.Img"),
		@WebInitParam (name = "catalog", 	value = "zjtv.SectionService"),
		@WebInitParam (name = "live", 		value = "ajaxjs.data.ext.LiveService"),
		@WebInitParam (name = "vod", 		value = "ajaxjs.data.ext.VodService"),
		@WebInitParam (name = "compere", 	value = "zjtv.CompereService"),
		@WebInitParam (name = "misc", 		value = "zjtv.MiscService"),
		@WebInitParam (name = "user", 		value = "ajaxjs.data.user.UserService"),
	}
)
public class zjtvServlet extends BaseServlet{
	private static final long serialVersionUID = 1L;
}

其中我们注意到,

urlPatterns = {"/service/*", "/admin_service/*"},

就是定义接口 URL 起始路径,因为使用了通贝符 *,所以可以允许我们 /service/news/、/service/product/200 形成各种各样的 REST 接口。

但是,我们不对 zjtvServlet 直接进行测试,而是其父类 BaseServlet 即可。个中原因是我们模拟像 WebServlet 这样的注解比较不方便。虽然是注解,但最终还是通过某种形式的转化,形成 ServletConfig 对象被送入到 HttpServlet.init 实例方法中去。于是我们采用后一种方法。

我们试观察 BaseServlet.init(ServletConfig config) 方法,还有每次请求都会执行的 doAction(),发现这两步所执行过程中需要用到的对象,及其方法是这样的,

/**
 * 初始化所有 JSON 接口
 * 为了方便测试,可以每次请求加载一次 js 文件,于是重载了一个子方法 private void init(String Rhino_Path)
 */
public void init(ServletConfig config) throws ServletException {
	init(Application.Rhino_Path);
	
	// 遍历注解的配置,需要什么类,收集起来,放到一个 hash 之中
	Enumeration<String> initParams = config.getInitParameterNames();
	while (initParams.hasMoreElements()) {
		String	initParamName 	= initParams.nextElement(),
				initParamValue 	= config.getInitParameter(initParamName);
		
		System.out.println("initParamName:" + initParamName + ", initParamValue:" + initParamValue);

		initParamsMap.put(initParamName, initParamValue);
	}
}
……
private void doAction(HttpServletRequest request, HttpServletResponse response){
	// 为避免重启服务器,调试模式下再加载 js
	if(Application.isDebug)init(Application.Rhino_Path);
	
	ajaxjs.net.Request.setUTF8(request, response);
	response.setContentType("application/json");
	
//		System.out.println(ajaxjs.net.Request.getCurrentPage_url(request));/

	Connection jdbcConn = DAO.getConn(getConnStr());

	try {
		Object obj = Application.jsRuntime.call("bf_controller_init", request, jdbcConn);
		if(obj != null)
			response.getWriter().println(obj.toString());
	} catch (Exception e) {
		e.printStackTrace();
		ajaxjs.Util.catchException(e, "调用 bf.controller.init(); 失败!");
	}
	
	output(request, response);
	try {
		jdbcConn.close();
	} catch (SQLException e) {
		e.printStackTrace();
	}
}

于是,我们遵循“依赖什么,模拟什么”的原则,让 Mockito 为我们生成模拟的对象,以假乱真。

首先,我们不能忘记这是一个 Web 项目,因此开头讲的那个 Listener 类也要首当其冲被初始化,才能有 Servlet 正确执行。于是,在 JUnit 单元测试的起始工作中,执行,

	@Before
	public void setUp() throws Exception {
		TestApplication app = new TestApplication();
		app.setUp();
		app.testContextInitialized();
	}

同时也把 setUp()、testContextInitialized() 手动执行一遍,因为之前的时候,我们是让 JUnit 或者 Tomcat 自动执行的。运行这一步之后,我们就初始化完毕侦听器 Listener 了。

这里所涉及的对象和方法比较多,下面我们逐一分解。

模拟 ServletConfig 对象

接着,怎么通过“模拟注解”来初始化 Servlet 配置呢?这里涉及到一个 Enumeration 对象的模拟,——其实也挺好办,方法如下,

	/**
	 * 初始化 Servlet 配置,这里是模拟 注解
	 * @return
	 */
private ServletConfig initServletConfig(){
		ServletConfig servletConfig = mock(ServletConfig.class);
		// 模拟注解
		Vector<String> v = new Vector<String>();
        v.addElement("news");
        when(servletConfig.getInitParameter("news")).thenReturn("ajaxjs.data.service.News");
        v.addElement("img");
        when(servletConfig.getInitParameter("img")).thenReturn("ajaxjs.data.service.subObject.Img");
        v.addElement("catalog");
        when(servletConfig.getInitParameter("catalog")).thenReturn("zjtv.SectionService");
        v.addElement("user");
        when(servletConfig.getInitParameter("user")).thenReturn("ajaxjs.data.user.UserService");

        Enumeration<String> e = v.elements(); 
		when(servletConfig.getInitParameterNames()).thenReturn(e);
		
		return servletConfig;
}

你可以定义更多业务对象,就像注解那样,结果无异。

模拟 Request 对象

下面所有虚拟的 Request 方法都可以按照你的项目配置进行修改

/**
 * 请求对象
 * @return
 */
private HttpServletRequest initRequest(){
    HttpServletRequest request = mock(HttpServletRequest.class);
    when(request.getPathInfo()).thenReturn("/zjtv/service/news");
    when(request.getRequestURI()).thenReturn("/zjtv/service/news");
    when(request.getContextPath()).thenReturn("/zjtv");
//        when(request.getSession()).thenReturn("/zjtv");
    when(request.getMethod()).thenReturn("GET");
    // 设置参数
    when(request.getParameter("a")).thenReturn("aaa");
    
    final Map<String, Object> hash = new HashMap<String, Object>();
    Answer<String> aswser = new Answer<String>() {  
        public String answer(InvocationOnMock invocation) {  
            Object[] args = invocation.getArguments();  
            return hash.get(args[0].toString()).toString();  
        }  
    };
    
    when(request.getAttribute("isRawOutput")).thenReturn(true);  
    when(request.getAttribute("errMsg")).thenAnswer(aswser);  
    when(request.getAttribute("msg")).thenAnswer(aswser);  
//        doThrow(new Exception()).when(request).setAttribute(anyString(), anyString());
    
    doAnswer(new Answer<Object>() {
        public Object answer(InvocationOnMock invocation) {
            Object[] args = invocation.getArguments();
            // Object mock = invocation.getMock();  
            System.out.println(args[1]);
            hash.put(args[0].toString(), args[1]);
            return "called with arguments: " + args;
        }
    }).when(request).setAttribute(anyString(), anyString());
    
    return request;
}

其中比较麻烦的 request.getAttribute() / setAttribute() 方法。鉴于 HttpServlet 是接口的缘故,我们必须实现一遍 getAttribute() / setAttribute() 的内部实现。此次我们只是简单地利用一个 map 来保存 reuqest.setAttribute() 的信息。然后使用 Mockito 的 Answer 接口获取真实的参数如何,从而让 request.getAttribute() 返回具体的值。

最初看到的做法是这样

	class StubServletOutputStream extends ServletOutputStream {
		public ByteArrayOutputStream baos = new ByteArrayOutputStream();
		public void write(int i) throws IOException {
			baos.write(i);
		}
		public String getContent() {
			return baos.toString();
		}
	}

上述是个内部类,实例化如下,

		StubServletOutputStream servletOutputStream = new StubServletOutputStream();
		when(response.getOutputStream()).thenReturn(servletOutputStream);
                ……doPost(request, response);
                byte[] data = servletOutputStream.baos.toByteArray();
                System.out.println("servletOutputStream.getContent:" + servletOutputStream.baos.toString());

我不太懂 Steam 就没深入了,再 Google 下其他思路,结果有人提到把响应结果保存到磁盘中,我觉得不是太实用,直接返回 String 到当前测试上下文,那样就好了。

// http://stackoverflow.com/questions/5434419/how-to-test-my-servlet-using-junit
		HttpServletResponse response = mock(HttpServletResponse.class);
		StubServletOutputStream servletOutputStream = new StubServletOutputStream();
		when(response.getOutputStream()).thenReturn(servletOutputStream);
		// 保存到磁盘文件 需要在 bs.doPost(request, response); 之后  writer.flush();
//		PrintWriter writer = new PrintWriter("d:\\somefile.txt");
		StringWriter writer = new StringWriter();
                when(response.getWriter()).thenReturn(new PrintWriter(writer));

测试后,用 writer.toString() 返回服务端响应的结果。

模拟数据库

怎么模拟数据库连接?可以想象,模拟数据库的工作量比较大,干脆搭建一个真实的数据库得了。所以有人想到的办法是用 Mockito 绕过 DAO 层直接去测试 Service 层,对 POJO 充血。参见:Java Mocking入门—使用Mockito

不过我当前的方法,还是直接连数据库。因为是使用 Tomcat 连接池的,所以必须模拟 META-INF/context.xml 的配置,其实质是 Java Naming 服务。模拟方法如下,

/**
	 * 模拟数据库 链接 的配置
	 * @throws NamingException
	 */
	private void initDBConnection() throws NamingException{
		 // Create initial context
        System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.naming.java.javaURLContextFactory");
        System.setProperty(Context.URL_PKG_PREFIXES, "org.apache.naming");  
        // 需要加入tomcat-juli.jar这个包,tomcat7此包位于tomcat根目录的bin下。
        InitialContext ic = new InitialContext();
        ic.createSubcontext("java:");
        ic.createSubcontext("java:/comp");
        ic.createSubcontext("java:/comp/env");
        ic.createSubcontext("java:/comp/env/jdbc");
        // Construct DataSource
        try {
			SQLiteJDBCLoader.initialize();
		} catch (Exception e1) {
			e1.printStackTrace();
		}

        SQLiteDataSource dataSource = new SQLiteDataSource();
        dataSource.setUrl("jdbc:sqlite:c:\\project\\zjtv\\WebContent\\META-INF\\zjtv.sqlite");
        
        ic.bind("java:/comp/env/jdbc/sqlite", dataSource);
	}

至此,我们就可以模拟一次 HTTP 请求,对接口进行测试了!

目录
相关文章
|
19天前
|
Java 测试技术 Android开发
课时148:junit测试工具
课时148介绍了JUnit测试工具的使用,包括定义、配置和编写测试程序。JUnit是流行的用例测试工具,用于确保代码稳定性。
|
7月前
|
XML Java 测试技术
Spring5入门到实战------17、Spring5新功能 --Nullable注解和函数式注册对象。整合JUnit5单元测试框架
这篇文章介绍了Spring5框架的三个新特性:支持@Nullable注解以明确方法返回、参数和属性值可以为空;引入函数式风格的GenericApplicationContext进行对象注册和管理;以及如何整合JUnit5进行单元测试,同时讨论了JUnit4与JUnit5的整合方法,并提出了关于配置文件加载的疑问。
Spring5入门到实战------17、Spring5新功能 --Nullable注解和函数式注册对象。整合JUnit5单元测试框架
|
5月前
|
Java 程序员 测试技术
Java|让 JUnit4 测试类自动注入 logger 和被测 Service
本文介绍如何通过自定义 IDEA 的 JUnit4 Test Class 模板,实现生成测试类时自动注入 logger 和被测 Service。
98 5
|
6月前
|
SQL JavaScript 前端开发
基于Java访问Hive的JUnit5测试代码实现
根据《用Java、Python来开发Hive应用》一文,建立了使用Java、来开发Hive应用的方法,产生的代码如下
123 6
|
7月前
|
测试技术
单元测试问题之使用TestMe时利用JUnit 5的参数化测试特性如何解决
单元测试问题之使用TestMe时利用JUnit 5的参数化测试特性如何解决
126 2
|
7月前
|
IDE Java 测试技术
单元测试问题之Mockito 3.4mock静态方法如何解决
单元测试问题之Mockito 3.4mock静态方法如何解决
436 1
|
7月前
|
Java 测试技术 Maven
单元测试问题之在Maven项目中引入JUnit 5和Mockito的依赖如何解决
单元测试问题之在Maven项目中引入JUnit 5和Mockito的依赖如何解决
433 1
|
7月前
|
测试技术
如何使用 JUnit 测试方法是否存在异常
【8月更文挑战第22天】
208 0
|
7月前
|
Java 测试技术 Maven
Junit单元测试 @Test的使用教程
这篇文章是一个关于Junit单元测试中`@Test`注解使用的教程,包括在Maven项目中添加Junit依赖、编写带有@Test注解的测试方法,以及解决@Test注解不生效的常见问题。
|
2月前
|
数据可视化 前端开发 测试技术
接口测试新选择:Postman替代方案全解析
在软件开发中,接口测试工具至关重要。Postman长期占据主导地位,但随着国产工具的崛起,越来越多开发者转向更适合中国市场的替代方案——Apifox。它不仅支持中英文切换、完全免费不限人数,还具备强大的可视化操作、自动生成文档和API调试功能,极大简化了开发流程。

热门文章

最新文章