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 请求,对接口进行测试了!

  • 2
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
Spring Boot是一个用于构建Java应用程序的开源框架,它提供了一种简化了配置的方式来快速构建应用程序。JUnit是一个用于编写和运行单元测试的开源测试框架,而Mockito是一个用于创建和管理模拟对象的Java库。 下面是一个使用Spring Boot、JUnitMockito进行单元测试的示例: 假设我们有一个UserService类,它依赖于一个UserRepository接口来访问数据库并进行一些操作。我们想要对UserService的方法进行单元测试。 首先,我们需要创建一个测试类,命名为UserServiceTest。在测试类中,我们将使用JUnit的注解来标记测试方法,并使用Mockito来创建模拟对象。示例代码如下: ```java @RunWith(MockitoJUnitRunner.class) public class UserServiceTest { @InjectMocks private UserService userService; @Mock private UserRepository userRepository; @Test public void testGetUserById() { // 配置模拟对象的行为 User user = new User("1", "John"); when(userRepository.findById("1")).thenReturn(user); // 调用被测试的方法 User result = userService.getUserById("1"); // 验证结果 assertEquals("John", result.getName()); } } ``` 在上面的示例中,我们使用了@RunWith注解来指定使用MockitoJUnitRunner运行测试,这样就能自动创建和管理模拟对象。使用@InjectMocks注解将被测试的对象自动注入到测试类中,使用@Mock注解创建模拟对象。 在testGetUserById方法中,我们首先使用when方法配置userRepository模拟对象的行为,表示当传入参数为"1"时,返回一个指定的User对象。 然后,我们通过调用userService的getUserById方法来测试该方法的逻辑。最后,使用assertEquals断言来验证结果是否符合预期。 以上就是一个使用Spring Boot、JUnitMockito进行单元测试的示例。通过使用Mockito创建模拟对象,我们可以更容易地测试各个方法的逻辑,而不依赖于实际的数据库。这样可以提高测试效率并确保代码的质量。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sp42a

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值