JUnit + Mockito 单元测试(三)

这里假设我们没有 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 请求,对接口进行测试了!

时间: 2024-10-15 01:45:01

JUnit + Mockito 单元测试(三)的相关文章

JUnit + Mockito 单元测试(二)(good)

  import org.junit.Test; import org.mockito.Matchers; import org.mockito.Mockito; import java.util.List; import java.util.Map; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.mockito.

JUnit + Mockito 单元测试(二)

JUnit 是单元测试框架.Mockito 与 JUnit 不同,并不是单元测试框架(这方面 JUnit 已经足够好了),它是用于生成模拟对象或者直接点说,就是"假对象"的工具.两者定位不同,所以一般通常的做法就是联合 JUnit + Mockito 来进行测试. 入门 首先是配置 Mock 对象,看看例子怎么写的. List mock = mock( List.class ); when( mock.get(0) ).thenReturn( 1 ); assertEquals( &q

JUnit + Mockito 单元测试(一)

未接触 JUnit 之前,曾经对茫茫的代码不知所措--哪怕是自己写的--多写注释?重构代码?甚至为一个方法去写一篇技术文章来解释?--这些都是试过,感觉不是"控制代码"的可行之道,甚至说"徒劳"的.关于单元测试(Unit test),之前亦略有所闻,感觉用处不大,因为对一个方法检测返回的结果是否正确,--有点无聊--心想,我写的方法当然能返回预期的结果,这还有说?不至于那么低级的错误也犯得着吧!?于是对所谓测试的东东感觉简直就是在增加工作量--我把代码写漂亮点就行了

Android最佳Mock单元测试方案:Junit + Mockito + Powermock

本文旨在从实践出发,引导开发者在Android项目中进行Mock单元测试. 什么是单元测试 单元测试由一组独立的测试构成,每个测试针对软件中的一个单独的程序单元.单元测试并非检查程序单元之间是否能够合作良好,而是检查单个程序单元行为是否正确. 为什么要进行单元测试 在敏捷开发大行其道的今天,由于时间紧,任务重,过分依赖测试工程师以及下列原因,导致单元测试不被重视,在开发流程中处于一个可有可无的尴尬境地. 1. 浪费的时间太多 1. 软件开发人员不应参与单元测试 1. 我是很棒的程序员,不需要进行

Eclipse学习4-在Eclipse中使用JUnit进行单元测试(上)

使用JUnit进行测试 JUnit是与Eclipse一起提供的一个开源测试框架.在同一个Project中,可以创建与其它class无异的"基于JUnit"的class,并使用此JUnit代码去测试project中的其它class.JUnit的此种使用方式能够为每位在此应用程序上工作的人员构建一组标准的测试.如果当更改了应用程序代码以后,他们所需做的工作仅仅是点击几下按钮来验证此应用程序是否依然能够通过标准测试. JUnit被用来测试代码,并且它是由能够测试不同条件的断言方法(asser

JUnit和单元测试入门简介

JUnit和单元测试入门简介 1.几个相关的概念 白盒测试--把测试对象看作一个打开的盒子,程序内部的逻辑结构和其他信息对测试人员是公开的. 回归测试--软件或环境的修复或更正后的"再测试",自动测试工具对这类测试尤其有用. 单元测试--是最小粒度的测试,以测试某个功能或代码块.一般由程序员来做,因为它需要知道内部程序设计和编码的细节. JUnit --是一个开发源代码的Java测试框架,用于编写和运行可重复的测试.他是用于单元测试框架体系xUnit的一个实例(用于java语言).主要

请问swing做的界面该怎么使用junit做单元测试呢?

问题描述 请问swing做的界面该怎么使用junit做单元测试呢? 解决方案 FEST-Swing是一个能够与JUnit集成的GUI测试框架.你可以研究研究.

junit测试单元测试swing 结果窗口只是闪一下就看不到了 求解。。

问题描述 junit测试单元测试swing 结果窗口只是闪一下就看不到了 求解.. 源代码: package com.Swing; import static org.junit.Assert.*; import java.awt.BorderLayout; import java.awt.Container; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import javax.swing.JB

Eclipse学习4-在Eclipse中使用JUnit进行单元测试(下)

使用JUnit测试一个应用程序现在已经准备好测试JN_test应用程序.为了测试,还需要使用JUnit Wizard创建一个新的class来扩展JUnit测试用例.要使用此wizard,请在Package Explorer 中的JN_test上单击右键,并且选择New->Other来打开一个New对话框,如图所示: 现在展开Java结点并选择JUnit,然后再选择JUnit Test Case,单击Next按钮,如图: 通常情况下JUnit类命名要和被它测试的类同名,并在其后面添加Test.所以