本文最后更新于:2020年12月30日 凌晨
前置要求
Java
Spring/SpringMVC
Maven
概略 单元测试/集成测试是软件开发时重要的一项流程,而 Spring 对于测试提供了非常强大的支持。
支持主流测试框架 Junit
/TestNG
支持在测试中使用依赖注入
支持在测试中事物自动回滚
支持使用各种注解增强功能
那么,测试基本上按照场景分为三种情况:
普通测试:不需要使用 Spring 容器的测试(工具类)
Dao/Service 层测试:需要使用 Spring 容器的依赖注入
Web 层测试:测试对外部提供的接口
这里新建一个用来测试的项目,吾辈将之丢到了 GitHub 上面
项目链接
你也可以自己创建一个基础的 Maven 项目,项目结构应当如下:
普通测试 假设吾辈有一个 SpringUtil
(路径是 /src/main/java/com/rxliuli/study/springtest/util/SpringUtil.java
) 工具类,想要测试怎么办呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 public class StringUtil { public static boolean isEmpty (String string) { return string == null || string.isEmpty(); } public static boolean isNotEmpty (String string) { return !isEmpty(string); } public static boolean isAnyEmpty (String... strings) { return Arrays.stream(strings) .anyMatch(StringUtil::isEmpty); } public static boolean isAllEmpty (String... strings) { return Arrays.stream(strings) .allMatch(StringUtil::isEmpty); } }
首先需要引入以下依赖
Junit4
:流行的 Java 测试框架。虽然吾辈个人更喜欢 TestNG
,但 Junit
的流行度要更高一点,SpringBoot
甚至将之默认引入了,所以这里使用 Junit
框架。
AssertJ
:流行的 Java 流畅式断言框架。Junit
也有自己的断言方法,但和 AssertJ
相比就是小巫见大巫了。
1 2 3 4 5 6 7 8 9 10 11 12 <dependency > <groupId > junit</groupId > <artifactId > junit</artifactId > <version > 4.12</version > <scope > test</scope > </dependency > <dependency > <groupId > org.assertj</groupId > <artifactId > assertj-core</artifactId > <version > 3.9.1</version > <scope > test</scope > </dependency >
注:生产环境中 version 应该放到 properties 节点下,这里只做演示。 此篇不对 Junit
和 AssertJ
进行详细的讲解,如果想要入门了解参考 Junit , AssertJ
然后创建一个对应的测试类 StringUtilTest
(/src/test/java/com/rxliuli/study/springtest/util/SpringUtilTest.java
)直接进行测试即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public class StringUtilTest { private String strNull = null ; private String strEmpty = "" ; private String strSome = "str" ; @Test public void isEmpty () { assertThat(StringUtil.isEmpty(strNull)) .isTrue(); assertThat(StringUtil.isEmpty(strEmpty)) .isTrue(); assertThat(StringUtil.isEmpty(strSome)) .isFalse(); } @Test public void isNotEmpty () { assertThat(StringUtil.isNotEmpty(strNull)) .isFalse(); assertThat(StringUtil.isNotEmpty(strEmpty)) .isFalse(); assertThat(StringUtil.isNotEmpty(strSome)) .isTrue(); } @Test public void isAnyEmpty () { assertThat(StringUtil.isAnyEmpty(strNull, strEmpty, strSome)) .isTrue(); assertThat(StringUtil.isAnyEmpty()) .isFalse(); } @Test public void isAllEmpty () { assertThat(StringUtil.isAllEmpty(strNull, strEmpty, strSome)) .isFalse(); assertThat(StringUtil.isAnyEmpty(strNull, strEmpty)) .isTrue(); } }
上面测试流程基本如下:
构建出测试需要的参数(非必需)
调用需要测试的方法
使用 AssertJ 对得到的结果进行断言
Dao/Service 层测试 准确的说是需要使用 Spring 容器的测试,测试方法有 2 种。
手动使用 ApplicationContext
去获取 Bean 然后进行测试
使用注解自动加载 Spring 测试环境
手动使用 ApplicationContext
去获取 Bean 然后进行测试 这里先演示手动使用 ApplicationContext
的做法进行测试,为了简化测试,这里直接使用 H2DB
和 SpringJdbcTemplate
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <dependency > <groupId > org.springframework</groupId > <artifactId > spring-context</artifactId > <version > 4.3.14.RELEASE</version > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-jdbc</artifactId > <version > 4.3.14.RELEASE</version > </dependency > <dependency > <groupId > com.h2database</groupId > <artifactId > h2</artifactId > <version > 1.4.196</version > <scope > test</scope > </dependency >
创建初始化 sql 文件 hsqldb/initDatabase.sql
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 drop table if exists user ;create table user ( id int auto_increment not null comment '编号' , name varchar (20 ) not null comment '名字' , sex boolean null comment '性别' , age int null comment '年龄' );insert into user (id, name, sex, age)values (1 , '琉璃' , false , 17 ), (2 , '月姬' , false , 1000 );
Spring 配置文件 spring/spring-context.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?xml version="1.0" encoding="UTF-8" ?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc ="http://www.springframework.org/schema/jdbc" xmlns:p ="http://www.springframework.org/schema/p" xmlns:context ="http://www.springframework.org/schema/context" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd" > <context:component-scan base-package ="com.rxliuli.study.springtest" /> <jdbc:embedded-database id ="dataSource" type ="H2" > <jdbc:script location ="classpath:hsqldb/initDatabase.sql" encoding ="UTF-8" /> </jdbc:embedded-database > <bean class ="org.springframework.jdbc.core.JdbcTemplate" id ="jdbcTemplate" p:dataSource-ref ="dataSource" /> </beans >
接下来我们可以编写实体类 com.rxliuli.study.springtest.entity.User
,dao 层 com.rxliuli.study.springtest.dao.UserDao
和 dao 对应的测试类 com.rxliuli.study.springtest.dao.UserDaoTest
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 public class User { private Integer id; private String name; private Boolean sex; private Integer age; public User () { } public User (String name, Boolean sex, Integer age) { this .name = name; this .sex = sex; this .age = age; } public User (Integer id, String name, Boolean sex, Integer age) { this .id = id; this .name = name; this .sex = sex; this .age = age; } public Integer getId () { return id; } public User setId (Integer id) { this .id = id; return this ; } public String getName () { return name; } public User setName (String name) { this .name = name; return this ; } public Boolean getSex () { return sex; } public User setSex (Boolean sex) { this .sex = sex; return this ; } public Integer getAge () { return age; } public User setAge (Integer age) { this .age = age; return this ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Repository public class UserDao { private final RowMapper<User> userRowMapper = (rs, rowNum) -> new User ( rs.getInt("id" ), rs.getString("name" ), rs.getBoolean("sex" ), rs.getInt("age" ) ); @Autowired private JdbcTemplate jdbcTemplate; public User get (Integer id) { return jdbcTemplate.queryForObject("select * from user where id = ?" , userRowMapper, id); } public List<User> listForAll () { return jdbcTemplate.query("select * from user" , userRowMapper); } }
注:这里直接使用了 Dao 类,生产过程中最好使用接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public class UserDaoTest { private UserDao userDao; @Before public void before () { ApplicationContext context = new ClassPathXmlApplicationContext ("classpath:/spring/spring-context.xml" ); userDao = context.getBean(UserDao.class); } @Test public void get () { int id = 1 ; User result = userDao.get(id); assertThat(result) .extracting(User::getId) .contains(id); } @Test public void listForAll () { List<User> userList = userDao.listForAll(); assertThat(userList) .isNotEmpty(); } }
手动加载的基本思路就是:
先加载 ApplicationContext 初始化 Spring 环境 注:这一步实际上就已经加载了 Spring 容器,并且使用 initDatabase.sql
初始化 h2 DB 了
使用 ApplicationContext 对象获得 UserDao 实例
调用被测试的方法
对结果进行断言
但这里实际上,ApplicationContext 是会被初始化两次的,所以会造成浪费和麻烦(例如初始化 sql 脚本也会被执行两次,当然这里吾辈先把 user 表删除后再创建的所以没事)。其实 Spring 早已想到了这一切,并为我们准备了解决方案。
使用 SpringTest
整合测试!
使用注解自动加载 Spring 测试环境 1 2 3 4 5 6 <dependency > <groupId > org.springframework</groupId > <artifactId > spring-test</artifactId > <version > 4.3.14.RELEASE</version > <scope > test</scope > </dependency >
使用 SpringTest 进行测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @ContextConfiguration(locations = "classpath:/spring/spring-context.xml") @RunWith(SpringJUnit4ClassRunner.class) public class UserDaoSpringTest { @Autowired private UserDao userDao; @Test public void get () { int id = 1 ; User result = userDao.get(id); assertThat(result) .extracting(User::getId) .contains(id); } @Test public void listForAll () { List<User> userList = userDao.listForAll(); assertThat(userList) .isNotEmpty(); } }
可以看到,这里我们甚至可以使用 Spring 的自动注入注解 @Autowired
了
当然,现在还有一个问题就是现在测试对数据库的影响是持久的,也就是说不能重复的测试。 例如删除了一个为 id 为 1 的用户,返回值应当是 1,但第二次删除时,因为 id 为 1 的用户已经不存在了,所以返回值是 0,然后就报错了 23333
我们可以测试一下
在 UserDao 中新增方法 deleteById
1 2 3 4 5 6 7 8 9 public int deleteById (Integer id) { return jdbcTemplate.update("delete from user where id = ?" , id); }
然后在测试类 UserDaoSpringTest
中添加两个删除测试方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void deleteById () { int result = userDao.deleteById(1 ); assertThat(result) .isGreaterThan(0 ); }@Test public void deleteByIdForTransaction () { int result = userDao.deleteById(1 ); assertThat(result) .isGreaterThan(0 ); }
然后运行测试类,你会得到一个错误
1 2 3 4 5 java.lang.AssertionError: Expecting: <0 > to be greater than: <0 >
所以我们需要让所有测试的操作都不影响到数据库,即 全局事物 + 默认回滚 。
首先需要在 spring-context.xml
中添加数据库事务管理的配置
1 2 3 4 <bean class ="org.springframework.jdbc.datasource.DataSourceTransactionManager" id ="dataSourceTransactionManager" p:dataSource-ref ="dataSource" /> <tx:annotation-driven transaction-manager ="dataSourceTransactionManager" />
在 UserDaoSpringTest
测试类上添加两个注解
1 2 3 4 @Transactional @Rollback
再次运行,一切便都正常了,是不是感觉很棒!但每个测试类头上都加那么一大堆注解也很麻烦,所以我们需要将之抽出一个父类直接继承就好了
以下是一个最简单的测试基类 BaseTest
(common.test.BaseTest)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = "classpath:/spring/spring-context*.xml") @Transactional @Rollback public abstract class BaseTest <BaseBean> { @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") @Autowired private BaseBean base; }
然后创建测试类 UserDaoBaseTest
(com.rxliuli.study.springtest.dao.UserDaoBaseTest) 继承 BaseTest
进行测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public class UserDaoBaseTest extends BaseTest <UserDao> { @Test public void get () { int id = 1 ; User result = base.get(id); assertThat(result) .extracting(User::getId) .contains(id); } @Test public void listForAll () { List<User> userList = base.listForAll(); assertThat(userList) .isNotEmpty(); } @Test public void deleteById () { int result = base.deleteById(1 ); assertThat(result) .isGreaterThan(0 ); } @Test public void deleteByIdForTransaction () { int result = base.deleteById(1 ); assertThat(result) .isGreaterThan(0 ); } }
运行结果也是一切正常呢,对 Dao/Service 需要加载 Spring 容器的测试暂且到这里便结束了。。。
Web 层测试 绝大部分时候,很多人喜欢写完代码就到前台页面直接看效果。但人眼是不一定准确的,而且可重复性/可靠性不足。如果是 API,大部分人或许会选择诸如 Postman , IDEA HttpClient 这一类的工具吧,但实际上,SpringTest 已经考虑到了对 Web 层的测试并集成了这些。
首先还是需要添加依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <dependency > <groupId > org.springframework</groupId > <artifactId > spring-webmvc</artifactId > <version > 4.3.14.RELEASE</version > </dependency > <dependency > <groupId > com.fasterxml.jackson.core</groupId > <artifactId > jackson-core</artifactId > <version > 2.9.5</version > </dependency > <dependency > <groupId > com.fasterxml.jackson.core</groupId > <artifactId > jackson-databind</artifactId > <version > 2.9.5</version > </dependency > <dependency > <groupId > com.fasterxml.jackson.core</groupId > <artifactId > jackson-annotations</artifactId > <version > 2.9.5</version > </dependency > <dependency > <groupId > com.fasterxml.jackson.module</groupId > <artifactId > jackson-module-jaxb-annotations</artifactId > <version > 2.9.5</version > </dependency > <dependency > <groupId > com.jayway.jsonpath</groupId > <artifactId > json-path-assert</artifactId > <version > 2.4.0</version > <scope > test</scope > </dependency >
设置 maven 打包时为 war
1 <packaging > war</packaging >
添加配置文件 spring/spring-mvc.xml
1 2 3 4 5 6 7 8 9 10 11 12 <?xml version="1.0" encoding="UTF-8" ?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc ="http://www.springframework.org/schema/mvc" xmlns:context ="http://www.springframework.org/schema/context" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd" > <mvc:annotation-driven /> <context:component-scan base-package ="com.rxliuli.study.springtest" > <context:include-filter type ="annotation" expression ="org.springframework.stereotype.Controller" /> <context:include-filter type ="annotation" expression ="org.springframework.web.bind.annotation.RestController" /> </context:component-scan > </beans >
添加 web 的根目录 /src/main/webapp
并在 webapp 目录下创建 web.xml
(WEB-INF/web.xml)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 <?xml version="1.0" encoding="UTF-8" ?> <web-app xmlns ="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version ="3.1" > <context-param > <param-name > contextConfigLocation</param-name > <param-value > classpath*:/spring/spring-context*.xml</param-value > </context-param > <listener > <listener-class > org.springframework.web.context.ContextLoaderListener</listener-class > </listener > <listener > <listener-class > org.springframework.web.context.request.RequestContextListener</listener-class > </listener > <filter > <filter-name > encodingFilter</filter-name > <filter-class > org.springframework.web.filter.CharacterEncodingFilter</filter-class > <init-param > <param-name > encoding</param-name > <param-value > UTF-8</param-value > </init-param > <init-param > <param-name > forceEncoding</param-name > <param-value > true</param-value > </init-param > </filter > <filter-mapping > <filter-name > encodingFilter</filter-name > <url-pattern > /*</url-pattern > </filter-mapping > <servlet > <servlet-name > springServlet</servlet-name > <servlet-class > org.springframework.web.servlet.DispatcherServlet</servlet-class > <init-param > <param-name > contextConfigLocation</param-name > <param-value > classpath*:/spring/spring-mvc*.xml</param-value > </init-param > <load-on-startup > 1</load-on-startup > </servlet > <servlet-mapping > <servlet-name > springServlet</servlet-name > <url-pattern > /</url-pattern > </servlet-mapping > </web-app >
至此,项目中便添加了 web 环境支持。
下面开始编写要测试的接口 UserController
(com.rxliuli.study.springtest.web.UserController)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @RestController public class UserController { @Autowired private UserDao userDao; @GetMapping("/user/{id}") public User get (@PathVariable("id") Integer id) { return userDao.get(id); } @PostMapping("/user/listForAll") public List<User> listForAll () { return userDao.listForAll(); } }
这时候启动 web 项目在浏览器中访问 localhost:8080/{上下文}/user/1
应当会得到一个 User 对象。
注:上下文 IDEA 默认为空,Eclipse 默认为项目名
然而测试却是有两种方法:
独立安装测试 手动加载单个 Controller,所以测试其他 Controller 中的接口会发生异常。但测试速度上较快,所以应当优先选择。
集成 Web 环境测试 将启动并且加载所有的 Controller, 所以效率上之于 BaseWebUnitTest 来说非常低下, 仅适用于集成测试多个 Controller 时使用。
独立安装测试 简单的独立安装测试类 UserControllerUnitTest
(com.rxliuli.study.springtest.web.UserControllerUnitTest)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = "classpath:/spring/spring-*.xml") @Transactional @Rollback @WebAppConfiguration public class UserControllerUnitTest { @Autowired private UserController userController; private MockMvc mockMvc; @Before public void before () { mockMvc = MockMvcBuilders.standaloneSetup(userController) .build(); } @Test public void testGet () throws Exception { Integer id = 1 ; mockMvc.perform( get("/user/" + id) ) .andExpect(status().isOk()) .andExpect(jsonPath("$.id" ).value(id)); } @Test public void listForAll () throws Exception { mockMvc.perform( post("/user/listForAll" ) ) .andExpect(status().isOk()) .andExpect(jsonPath("$" ).isArray()) .andExpect(jsonPath("$" ).isNotEmpty()); } }
集成 Web 环境测试 简单的独立安装测试类 UserControllerIntegratedTest
(com.rxliuli.study.springtest.web.UserControllerIntegratedTest)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = "classpath:/spring/spring-*.xml") @Transactional @Rollback @WebAppConfiguration public class UserControllerIntegratedTest { @Autowired private WebApplicationContext context; private MockMvc mockMvc; @Before public void before () { mockMvc = MockMvcBuilders.webAppContextSetup(context) .build(); } @Test public void testGet () throws Exception { Integer id = 1 ; mockMvc.perform( get("/user/" + id) ) .andExpect(status().isOk()) .andExpect(jsonPath("$.id" ).value(id)); } @Test public void listForAll () throws Exception { mockMvc.perform( post("/user/listForAll" ) ) .andExpect(status().isOk()) .andExpect(jsonPath("$" ).isArray()) .andExpect(jsonPath("$" ).isNotEmpty()); } }
其实从上面可以看出来主要就是获得 MockMvc 的方式不同,所以其实也可以抽出来公共的测试父类。这里就不再赘述,具体的做法可以参考 测试基类
那么,有关使用 Spring 进行测试的问题就像说到这里啦
附:用了 SpringBoot 之后才觉得 Spring 的各种配置好麻烦。。。