[JavaWeb]_[初级]_[对Jfinal框架的Controller进行单元测试]

Posted infoworld

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[JavaWeb]_[初级]_[对Jfinal框架的Controller进行单元测试]相关的知识,希望对你有一定的参考价值。

场景

  1. 开发JavaWeb网站时,对Service层使用JUnit测试接口还是比较容易的,因为它基本只涉及了简单的数据库操作和业务单元操作,不涉及复杂的页面导航校验,最主要的还是不需要启动应用服务器Tomcat来运行单元测试。

  2. 只测试Service层是不够的,完整的单元测试也应该包含Controller(控制层)的测试,这样可以避免耗时的浏览器点击验证流程。那么这一层如何高效的进行单元测试呢?

说明

  1. 一般应用框架都会有Controller,它会用到容器的HttpServletRequest,HttpServletSession存储页面数据,还要进行数据校验,组件间的事件通讯等。在JFinal框架里也有基类com.jfinal.core.Controller来处理HttpServletRequestHttpServletSession.

    
    @Path("/blog")
    @Before(BlogInterceptor.class)
    public class BlogController extends Controller 
    
        @Before(BlogValidator.class)
        public Integer save() 
        	Blog blog = getBean(Blog.class);
        	boolean success = blog.save();
        	redirect("/blog");
        	return (success)?blog.getId():0;
        
    ...
    
    
  2. 现在看看save()方法用到了ControllergetBean()redirect()方法。getBean主要是在com.jfinal.core.Injector类里用到了injectBean()方法,这个方法主要用到了HttpServletRequest外部对象。这个request主要用到了getParameterMapgetParameter方法,所以只要Mock这两个方法返回特定的值就可以了。

    public static <T> T injectBean(Class<T> beanClass, String beanName, HttpServletRequest request, boolean skipConvertError) 
        ...
        Map<String, String[]> parasMap = request.getParameterMap();
        ...
        String paraValue = request.getParameter(paraName);
    
    
  3. 之前在android开发时使用的是mockitoMock测试框架,通过在pom.xml里导入. 这里由于需要操作HttpServletRequest,所以还需要导入servlet-apiJUnit无论何时单元测试都是必须的。

    <!-- junit 单元测试 -->
    <dependency>
    	<groupId>junit</groupId>
    	<artifactId>junit</artifactId>
    	<version>4.13.2</version>
    	<scope>test</scope>
    </dependency>
    <!-- mockito -->
    <dependency>
    	<groupId>org.mockito</groupId>
    	<artifactId>mockito-core</artifactId>
    	<version>3.9.0</version>
    	<scope>test</scope>
    </dependency>
    
    <!-- mockito 的junit适配器 -->
    <dependency>
    	<groupId>org.mockito</groupId>
    	<artifactId>mockito-junit-jupiter</artifactId>
    	<version>3.9.0</version>
    	<scope>test</scope>
    </dependency>
    
    <dependency>
    	<groupId>javax.servlet</groupId>
    	<artifactId>javax.servlet-api</artifactId>
    	<version>4.0.1</version>
    </dependency>
    
  4. 先说一下Mock框架基本上都会实现两点功能:1. 实现接口类的匿名类,并重载接口类的所有方法,添加空默认实现。 2. 实现接口类的匿名类,默认重载调用原方法,可以Mock某个指定的方法。 这两种方式使用Mock库都可以指定当方法调用时,可以返回某个指定的值。一般实现Mock子类,技术上会使用字节码修改或匿名代理两种方法来完成。使用Mock库可以快速的实现接口的方法而无需自己(通过匿名类)去添加空实现,减少了源码的数量,可以有效的专注业务逻辑上的编码。

  5. 在创建Mock类之后就是填充返回的Map<String, String[]>类型和Map<String, String>类型的数据,分别对应着request.getParameterMap()request.getParameter的返回值。也就是可以模拟设置request里的参数值,方便save()方法的调用。

    HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
    
    Map<String, String[]> params = new HashMap<>();
    params.put("blog.title",null);
    params.put("blog.content",null);
    
    Map<String, String> paramValue = new HashMap<>();
    paramValue.put("blog.title","This is Title");
    paramValue.put("blog.content","This is content");
    
    when(request.getParameterMap()).thenReturn(params);
    when(request.getParameter("blog.title")).thenReturn(paramValue.get("blog.title"));
    when(request.getParameter("blog.content")).thenReturn(paramValue.get("blog.content"));
    
    oc.setHttpServletRequest(request);
    
  6. save()方法调用成功后,接下来就是断言是否保存新数据成功。

    Integer _id = oc.save();
    Assert.assertNotNull(_id);
    Assert.assertTrue(_id.intValue() > 0);
    Blog blog = service.findById(_id);
    log.info("blog id: "+_id);
    Assert.assertEquals(blog.getStr("content"),"This is content");
    
  7. 运行测试用例,单击testSave()方法前面的右三角,选择Debug testSave(), 有以下输出:

        [INFO]-[Thread: main]-[com.demo.blog.test.TestBlogController.testSave()]: blog id: 34
    

匿名类

  1. 使用创建匿名类的方法重载某些不重要的方法。比如jfinal这里的redirect转发到某个页面渲染不需要测,所以BlogController里调用的redirect("/blog")不做任何处理。

    BlogController oc = new BlogController()
        @Override
        public void redirect(String url) 
    
        
    ;
    
    

Mock类

  1. spy()方式的Mock对象创建,调用原来的方法,参数是自己创建的实例。而mock()方式创建HttpServletRequest是完全Mock一个匿名新子类,调用Mock的空方法。这里Mock方法doNothing().when(oc).redirect(anyString())也是为了调用redirect()时什么都不做。

    BlogController oc = Mockito.spy(new BlogController());
    doNothing().when(oc).redirect(anyString());
    HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
    

例子

BlogController.java

package com.demo.blog;

import com.jfinal.aop.Before;
import com.jfinal.aop.Inject;
import com.jfinal.core.Controller;
import com.jfinal.core.Path;
import com.demo.common.model.Blog;

/**
 * 本 demo 仅表达最为粗浅的 jfinal 用法,更为有价值的实用的企业级用法
 * 详见 JFinal 俱乐部: https://jfinal.com/club
 * 
 * 所有 sql 与业务逻辑写在 Service 中,不要放在 Model 中,更不
 * 要放在 Controller 中,养成好习惯,有利于大型项目的开发与维护
 */
@Path("/blog")
@Before(BlogInterceptor.class)
public class BlogController extends Controller 
	
	@Inject
	BlogService service;
	
	public void index() 
		setAttr("blogPage", service.paginate(getParaToInt(0, 1), 10));
		render("blog.html");
	
	
	public void add() 
	
	
	/**
	 * save 与 update 的业务逻辑在实际应用中也应该放在 serivce 之中,
	 * 并要对数据进正确性进行验证,在此仅为了偷懒
	 */
	@Before(BlogValidator.class)
	public Integer save() 
		Blog blog = getBean(Blog.class);
		boolean success = blog.save();
		redirect("/blog");
		return (success)?blog.getId():0;
	
	
	public void edit() 
		setAttr("blog", service.findById(getParaToInt()));
	
	
	/**
	 * save 与 update 的业务逻辑在实际应用中也应该放在 serivce 之中,
	 * 并要对数据进正确性进行验证,在此仅为了偷懒
	 */
	@Before(BlogValidator.class)
	public void update() 
		getBean(Blog.class).update();
		redirect("/blog");
	
	
	public void delete() 
		service.deleteById(getParaToInt());
		redirect("/blog");
	

TestBlogController.java

package com.demo.blog.test;

import com.demo.blog.BlogController;
import com.demo.blog.BlogService;
import com.demo.common.model.Blog;
import com.jfinal.aop.Aop;
import com.jfinal.plugin.activerecord.ActiveRecordPlugin;
import com.jfinal.plugin.druid.DruidPlugin;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.junit.Assert;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.jupiter.MockitoExtension;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
public class TestBlogController 

    private static Logger log = Logger.getLogger(TestBlogController.class);

    DruidPlugin dp;
    ActiveRecordPlugin arp;
    BlogService service;

    @BeforeEach
    public void onSetUp()

        dp = new DruidPlugin("jdbc:mysql://localhost/jfinal_demo?characterEncoding=utf8&useSSL=false&zeroDateTimeBehavior=convertToNull", "root", "");
        arp = new ActiveRecordPlugin(dp);
        arp.setShowSql(true);
        arp.setDevMode(true);
        arp.addMapping("blog", Blog.class);

        // 与 jfinal web 环境唯一的不同是要手动调用一次相关插件的start()方法
        dp.start();
        arp.start();

        service = Aop.get(BlogService.class);
        log.setLevel(Level.INFO);

        MockitoAnnotations.openMocks(this);
    

    @AfterEach
    public void onTearDown()
        arp.stop();
        dp.stop();
    

    @Test
    public void testSave()

//        // 注意,也可以使用匿名类
//        BlogController oc = new BlogController()
//            @Override
//            public void redirect(String url) 
//
//            
//        ;

        BlogController oc = Mockito.spy(new BlogController());
        doNothing().when(oc).redirect(anyString());
        HttpServletRequest request = Mockito.mock(HttpServletRequest.class);

        Map<String, String[]> params = new HashMap<>();
        params.put("blog.title",null);
        params.put("blog.content",null);

        Map<String, String> paramValue = new HashMap<>();
        paramValue.put("blog.title","This is Title");
        paramValue.put("blog.content","This is content");

        when(request.getParameterMap()).thenReturn(params);
        when(request.getParameter("blog.title")).thenReturn(paramValue.get("blog.title"));
        when(request.getParameter("blog.content")).thenReturn(paramValue.get("blog.content"));

        oc.setHttpServletRequest(request);
        Integer _id = oc.save();
        Assert.assertNotNull(_id);
        Assert.assertTrue(_id.intValue() > 0);
        Blog blog = service.findById(_id);
        log.info("blog id: "+_id);
        Assert.assertEquals(blog.getStr("content"),"This is content");
    

下载

https://download.csdn.net/download/infoworld/85134705

参考

  1. JFinal 文档、资料、学习、API,Maven 基础

  2. 徒手撸一个Mock框架(八)—— 调用原始方法

  3. 使用Mockito的Mock Void方法_cunfeng7797的博客

  4. Java Mockito.spy方法代碼示例

  5. Mockito framework site

  6. 对Jfinal框架的Service层进行单元测试

以上是关于[JavaWeb]_[初级]_[对Jfinal框架的Controller进行单元测试]的主要内容,如果未能解决你的问题,请参考以下文章

[JavaWeb]_[初级]_[对Jfinal框架的Controller进行单元测试]

[JavaWeb]_[初级]_[对Jfinal框架的Controller进行单元测试]

[JavaWeb]_[初级]_[搭建jfinal_4.9.15_demo_for_maven开发环境]

[JavaWeb]_[初级]_[搭建jfinal_4.9.15_demo_for_maven开发环境]

[JavaWeb]_[初级]_[jfinal集成ehcache缓存]

[JavaWeb]_[初级]_[jfinal集成ehcache缓存]