Spring 单元测试 (JUnit4)

之所以要单独讲解 Spring 的单元测试,是因为普通的 junit 单元测试并不能很好的与 spring 容器结合起来,因为很多对象都是需要从 spring 容器中自动注入进来的,而 Spring 也考虑到了这个问题,所以提供了一个专门的 Spring Runner 运行器,当我们使用 Spring Runner 来运行测试类时,就可以使用 Spring 的一些 bean 注解,来自动装配容器中的 bean,然后进行测试;而且对于 spring mvc 模块,spring 也提供了相应的支持,spring mvc 的测试难在程序的运行和测试都需要一个 servlet 容器环境,这个实在不好搞,所以如果不借助 spring 的测试模块,对于 spring mvc 的测试我们将束手无策,只能使用最原始的方式:使用浏览器或其它客户端进行手动测试,测试效率不高先不说,测试的结果也没有信服力,因为手动测试很难覆盖所有的测试点。

预备知识

  • Java 语言:由一个一个类组成,Class 是 Java 的一等公民,JRE 就是 Java 已经编写好的一些基础类,如 String、Object、System、java.io、java.util、java.net 等等,便于我们编写其它更高级的应用程序,这些东西称为 Java 的标准类库,当然我们也经常需要使用第三方类库,如 Spring、Apache Commons,总之你记住,Java 就是由一个一个的类组成的,基本单位就是 Class。
  • Spring 容器:Spring 有两个核心概念:IoC 和 AOP,但其实最基本的东西还是 IoC,也就是所谓的 bean 容器,没错,Spring 就是一个容器,这个容器里面装的都是 bean/pojo(Java 对象),所有的一切都是基于这个 bean/pojo 容器展开的,没有这个 bean 容器就不能称为 Spring。注意,只有在 bean 容器中的 java 实例才会被 spring 管理,无论何时都是这样,如果一个对象没有注册在 bean 容器中,那么 spring 是不会对它进行任何操作的。spring 容器的基本成员就是 bean(java 对象实例),注册为 spring 的 bean 有两种方式,一种是使用 xmlconfig 的 <bean> 标签来注册,或者使用 javaconfig 的 @Bean 注解的方法进行注册;另一种方法则是启用 spring 组件扫描功能,然后在对应的 Bean/Pojo 类上使用 @Component、@Controller、@Service、@Repository 等注解来告诉 Spring 框架,自动为其实例化(注意 Spring 只会该类的无参构造函数,无法传入其它参数)并注册到 bean 容器中。Spring 的装配就是指一个 bean 对象需要一些依赖对象,而 bean 容器中有这些依赖对象,所以就直接引用这些依赖对象,而不是新创建一个,这就叫做装配,而自动装配则是 Spring 会根据 name 或 type 来自动在 bean 容器中查找当前对象所依赖的对象(通常就是使用 @Autowired 注解来告诉 Spring 框架哪些构造函数的参数、setter 方法对应的属性需要由 Spring 来自动装配),如下:
    • 手动装配:在 xml/java-config 中,手动引用 bean 容器中的其它 bean 对象。
    • 自动装配:在 xml-config 中,使用 bean 元素的 autowire 属性来自动查找容器中合适的 bean。
    • 注解装配:启用注解驱动功能,然后直接在 bean 类上使用 @Autowired@Resource@Inject 来自动装配。
  • Servlet 容器:Servlet 容器(JSP 和静态资源本质上都会被 Servlet 给处理)常见的就是 Tomcat,容器这个词我们已经非常熟悉了,所谓容器就是装东西的东西,比如 Spring 容器就是装 java 对象的,而 Servlet 容器则是装 servlet 对象的。servlet 就是指实现了 javax.servlet.Servlet 接口的类的一个实例;和 Spring 容器一样,Servlet 容器中的 servlet 也是单例模式的,而 Servlet 容器又是多线程模型的,所以不要将需要修改的数据存放为 servlet 对象的实例变量,而应该放在 servlet 对象的 service() 方法中,因为函数的每次调用都是一个独立的栈结构,不会出现线程安全问题。servlet 对象的核心方法就是 void service(req, resp) 方法,这就是所谓的“服务方法”,req 就是当前请求的抽象表示,而 resp 则是当前请求对应的响应的抽象表示。而 Servlet 容器本质就是一个 java 进程,我们只需要提供 servlet 类,这个 Servlet 容器就能正常运行,并提供 Web 服务。大概流程和原理:Servlet 容器启动后,会创建一个线程池,监听某一个公共端口,如 8080;当一个客户端请求到达时,容器会从线程池中随机挑选一个线程来执行对应的 servlet 实例的 service() 方法(如果 servlet 实例还未创建则先创建),在此期间如果又有另外一个客户端请求到达,那么容器也是从线程池中随机挑选一个线程来执行对应的 servlet 实例的 service() 方法。所以 servlet 也可以认为是一个独立的 web 服务类,servlet 类并不能单独运行,它必须运行在 servlet 容器中。

OK,啰嗦了这么多,主要还是为了让大家搞清楚 spring 应用程序的测试难在哪?现在我们清楚了:

  • 对于普通 spring 程序:需要 spring 容器,才能进行测试。
  • 对于 spring mvc 程序:需要 spring 容器和 servlet 容器,才能进行测试。

Spring 测试模块(spring-test)对上述两种情况都提供了很好的支持,借助 spring-test 模块,我们就能够很好的进行 spring 应用的测试了。

Spring 程序测试

junit 中,所有的 test-case 都是通过 Runner 运行的,我们可以在测试类上使用 @RunWith(MyRunner.class) 来指定使用 MyRunner 运行器来运行当前测试类,如果未使用 @RunWith 注解,则 junit 会使用默认的 Runner 来运行。而 Spring 的 test 模块也是这个原理,Spring 提供了 2 个 junit runner:SpringJUnit4ClassRunnerSpringRunner;它们的作用都是一样的,SpringRunner 是 SpringJUnit4ClassRunner 的子类,SpringRunner 是 spring 4.3 版本才加入的,需要与 junit 4.12 及以上版本一起使用,SpringRunner 是 SpringJUnit4ClassRunner 的别名,如果 SpringRunner 可用,那么建议选择 SpringRunner,因为好记且简洁。

如果一个测试类被 @RunWith(SpringRunner.class) 标注了,则意味着这个测试类运行在 spring 容器中,然后我们就可以使用 @Autowired、@Resource 等注解来自动装配 spring 容器中的 bean 对象,进行测试,而不需要像以前一样,手动创建 ApplicationContext 上下文,然后调用 context 对象的 getBean() 方法来获取 bean 对象,这太麻烦了。

当然,仅仅有一个 @RunWith(SpringRunner.class) 也是不行的,因为 Spring 容器运行需要一个 spring.xml 或 Config.class,因为我们的 bean 对象都是从这个配置文件、配置类中获取的。所以我们还需要使用 Spring 提供的 @ContextConfiguration 注解来加载基于 xml 或基于 java 的 spring 配置文件,以初始化 spring 容器,如下,value/locations 用于指定基于 xml 形式的配置文件,而 classes 属性则用于指定基于 java 形式的配置文件:

简单例子:

SpringMVC 程序测试

相比普通的 Spring 程序测试,SpringMVC 程序的测试还需要一个 servlet 环境;普通 spring 程序还可以不借助 spring-test 模块,因为我们可以很容易的创建出 ApplicationContext 对象;但是 spring mvc 程序不行,servlet 环境不好搞,最容易想到的方法就是运行 tomcat,但是这也太复杂了,所以这时候就必须使用 spring-test 模块来进行 spring mvc 应用程序的测试了。

spring-test 对于 spring mvc 应用程序提供了很好的测试支持,估计各位也知道,servlet 环境启动需要一些时间,这通常会导致测试效率的下降,因为人的忍耐是有限的,而 spring-test 为了避免启动真实 servlet 环境的耗时长问题,它选择了使用 mock 对象,所谓 mock 对象就是模拟对象,也就是说 spring-test 提供的是一个模拟的 servlet 容器环境,虽然是模拟的,但是功能也很齐全,而且启动速度很快,相比真实的 servlet 容器来说。

为测试类开启 spring + servlet 环境支持也很简单,只需要在原有的 spring 测试类基础上,再添加一个 @WebAppConfiguration 注解即可。这个注解的意思是为当前测试类启用 servlet 运行环境(当然是模拟的环境,速度相对较快),该注解只有一个属性 value,用于指定 webapp 目录的路径,对于 maven 项目,那就是 src/main/webapp,而 @WebAppConfiguration 的 value 默认值就是 src/main/webapp,所以我们无需手动指定它。

简单例子:

WebApplicationContext 就是我们的 spring 上下文对象(web 环境),该上下文对象可以直接使用 @Autowired 注解自动装配,然后就是 MockMvc 模拟对象,通常为了方便,我们会在 @Before 方法中根据 context 对象创建它;然后就是我们的 test 方法,通过 mockMvc 的 perform 方法来发起一个 http 请求,比如上面的 get /employees,我们可以对其返回值进行 andDo() 操作,意思是在请求完成后打印相关的 request、response 信息(当然没有 html 视图,个人感觉不太直观),也可以调用 andExpect() 方法对响应结果进行断言,如上面的意思为:当前 uri 对应的 view 名为 employee-list,且当前响应的状态码为 200 OK,只要有一个不对,测试就会失败,然后我们可以在控制台上看到相关的错误信息。

个人感觉还不如直接浏览器测试来得直接,这种测试方式看不到 html 页面(也许是姿势不对吧),这个大家了解一下就可以了。

spring-test 事务管理
测试 ssm 程序时,通常都会涉及到数据库操作,这就有一个问题:每次我们测试之后,都需要将相关的数据库更改进行回滚,便于后续的测试,这其实是非常不方便的;现在,我们可以借助 spring-test 的贴心功能来自动回滚测试方法中的数据库操作(使用注解声明即可,简单、直观、方便)。并且 spring-test 模块对于测试类中的 @Test 方法的数据库操作,其实是会默认进行回滚操作的(当然是在开启了事务的前提下),简直不要太棒。

主要涉及到的注解为:

  • @TransactionConfiguration:标注在测试类上,常用属性为 defaultRollback,默认值为 true,表示测试方法执行后会自动进行回滚,如果为 false 则会自动进行提交。从 spring 4.2 版本开始,该注解被弃用了,spring 建议我们使用 @Rollback@Commit 注解来替代它,详见下。
  • @Rollback:可以标注在测试类或者测试方法上,属性为 value,默认值为 true,表示测试方法会在执行完成之后执行回滚,如果设为 false 则表示会自动进行提交操作;从 spring 4.2 起,可以使用 @Commit 来替代 @Rollback(false),它们的作用完全是一样的。类上面的 @Rollback/@Commit 注解提供一个默认值,可以被方法上的 @Rollback/@Commit 注解给覆盖。
  • @Commit:同 @Rollback,相当于 @Rollback(false),可以用在类上或方法上,表示事务会自动进行提交操作。

然后我们可以在测试类/方法上使用 @Transactional 注解,开启声明式事务。方法上的 @Transactional 优先级高于类上的 @Transactional。

旧版本的例子:

新版本的例子:

输出结果:

其中 @BeforeTransaction@AfterTransaction 也是 spring-test 提供的注解,注意它们与 @Before、@After 注解的执行顺序:

  • @BeforeTransaction:在事务开启之前执行,从输出结果也看得出来,此时没有开启事务。
  • @Before:在事务开启之后且在测试方法之前执行,即在 @Test 前执行,此时已经有一个事务。
  • @Test:在 @Before 方法之后执行,此时也有一个事务,@Before、@Test、@After 都是同一个事务。
  • @After:在 @Test 方法之后执行,此时也有一个事务,@Before、@Test、@After 也都是同一个事务。
  • @AfterTransaction:在事务完成后执行,此时事务已经提交或回滚,可以在方法中测试事务提交是否成功。

因为默认就是会执行数据库回滚操作,所以我们可以省略 @Rollback 注解。另外,我们通常会使用 @Before + @Test + @After 三个注解方法(使用默认策略:自动回滚事务),在 before 方法中打印数据表,然后执行 test 方法,然后在 after 方法中打印数据表,这样就可以对比 test 方法是否执行正确,是否符合我们预期的输出,因为在事务关闭之前,事务会被自动提交,所以我们无需手动去回滚或提交事务,这是极好的。