如何写好Spring /Spring Boot 单元测试

前言

初入公司接手项目,当写完一个小功能模块时,需要对此功能进行测试,因为不是http接口,所以也没有办法用Postman测试,所以需要编写单元测试用例,之前只接触过Spring Boot的项目,也简单写过一些小的测试用例,但是通过研究公司里前辈们的写法及其搜索了一些文章发现,里面还是有很多值得挖掘和学习的地方。因此有了这篇文章的诞生。

一、测试的分类

  1. 单元测试

    • 单元测试又可以称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。可以根据实际情况判定其具体含义,如C语言中单元指一个函数,Java里指一个类,图形化的软件中可以指一个窗口或一个菜单等,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。
  2. 集成测试

    • 集成测试是在单元测试的基础上延申而来,即对程序模块采用一次性或增值方式组装起来,对系统的接口进行正确性检验的测试工作。整合测试一般在单元测试之后、系统测试之前进行。实践表明,有时模块虽然可以单独工作,但是并不能保证组装起来也可以同时工作
  3. 系统测试

    • 系统测试是将需测试的软件,作为整个基于计算机系统的一个元素,与计算机硬件、外设、某些支持软件、数据和人员等其他系统元素及环境结合在一起测试。

二、测试所需要的工具

  • Junit

  • Spring(Spring的项目)

  • IDEA

三、如何写好单元测试

  通过搜索一些文章不难发现,大多数文章都在用SpringBootTest注解(Spring Boot项目)或者使用ContextConfiguration注解加载bean 的XML文件(Spring项目),其实这种方式如果论可行性完全说得通,起码测试功能是具备的。

  这种方式是告诉Spring/Spring Boot 我要加载整个项目程序的上下文,你要给我准备好,所以这种方式就需要去扫描所有的Bean文件,加载更多的实例到容器中来,那么就向相当于启动了整个项目,如果项目非常大,那么需要启动耗时很长;同时,如果牵扯到多人开发,可能需要用到其他人写的bean,如果此时bean还没有写好,那么加载到bean 容器里面我们也是没用的;同时如果我们要测试功能模块不需要依赖其他的Bean或者仅仅依赖几个Bean,那么通过上述方式进行单元测试就有种杀鸡焉用宰牛刀的感觉了。

  如何根据实际情况写好测试用例?这里附上Spring 官网关于单元测试的说明截图:

image-20220903230120283

翻译了一下:

  与传统 Java EE 开发相比,依赖注入应该使您的代码对容器的依赖更少。组成您的应用程序的 POJO 应该可以在 JUnit 或 TestNG 测试中进行测试,使用 new 运算符实例化对象,无需 Spring 或任何其他容器。您可以使用模拟对象(结合其他有价值的测试技术)来单独测试您的代码。如果您遵循 Spring 的架构建议,那么代码库的干净分层和组件化将有助于更轻松的单元测试。例如,您可以通过存根或模拟 DAO 或存储库接口来测试服务层对象,而无需在运行单元测试时访问持久数据。

  真正的单元测试通常运行得非常快,因为不需要设置运行时基础设施。强调真正的单元测试作为开发方法的一部分可以提高您的生产力。您可能不需要测试章节的这一部分来帮助您为基于 IoC 的应用程序编写有效的单元测试。然而,对于某些单元测试场景,Spring 框架提供了模拟对象和测试支持类,本章将对此进行介绍。

  根据自己的理解和查询网上的资料,可以得知,官方不推荐通过运行整个项目的方式来执行测试用例,因为通常情况下真正的单元测试运行速度非常快,尽量使用new的操作来实例化对象,根本不需要容器,如果遇到一些数据库依赖或者其他未完成的依赖,官方推荐我们使用Mock注解模拟对象来完成单元测试。根据官方的建议结合代码,下面分析一个例子具体感受下。

四、测试单元例子

首先定义service接口及其实现类

public interface StudentService {

   String getStudentName();
}
@Component(value = "studentServiceImpl")
public class StudentServiceImpl implements StudentService {


    @Override
    public String getStudentName(){
      return "张三";
   }
}

 这里为了方便起见,直接返回一个学生名字--张三。同时在接口实现类中添加@Component注解表明这是一个bean,需要交给IOC容器去管理。下面开始编写我们的测试类,其中测试类的写法有多种:

例子1

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:bean.xml"})
public class StudentTest {

   @Resource
   private StudentServiceImpl studentServiceImpl;

   @Test
   public void getStudentName() {
      System.out.println(studentServiceImpl.getStudentName());
   }
}

熟悉Spring的同学应该清楚,首先RunWith注解来源于Junit,就是一个运行器,具体要运行什么呢,需要我们指定,在这里指定使用SpringJUnit4ClassRunner,告诉我们,在测试开始的时候自动创建Spring的应用上下文,那具体从哪里加载bean呢?这里我们使用ContextConfiguration注解,通知Spring去bean.xml文件里面去加载,文件内容如下:

<?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: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/context https://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="com.qhw.example" />
</beans>

component-scan 就代表Spring需要去com.qhw.example包下及其子包下去寻找是否有@Component、@Service等等注解,如果有的话,就把注解标注的类当作一个bean加载到IOC容器里面去。当然此处还有一些子标签可以用,不再说明。

最终通过这种方式,把我们的StudentServiceImpl类加载到容器中,同时测试的时候通过@Resource注解加载到测试用例中来,进而调用其getStudentName方法输出学生的名字。

如果换成SpringBoot框架,那就更简单了,在测试类中将ContextConfiguration注解替换成SpringBootTest注解就会自动扫描Bean

  • 这种方式可以实现测试的功能,但是需要加载整个Spring项目,由于此处写的Demo比较简单,感觉不出来,在实际的开发环境中,项目启动需要加载一系列的东西,比如Spring上下文环境、注册中心、远程调用等等,大概跑完一个简单的单元测试需要耗时三四秒的时间,其中很大一部分完全是跟单元测试不相干的,所以根据官方的建议,我们需要对其进行改进。

例子2

接口及其实现类不变,仅仅改动测试类,代码如下:

public class StudentTest {

   private StudentServiceImpl studentServiceImpl;

   @Before
   public void setBean(){
      this.studentServiceImpl=new StudentServiceImpl();
   }

   @Test
   public void getStudentName() {
      System.out.println(this.studentServiceImpl.getStudentName());
   }
}

通过对比可以看到几个变化

  • 不再使用依赖注入的方式,而是通过new的方式去生成一个对象
  • 使用@Before注解,其作用是在执行测试函数的时候,先去执行此注解标记的前置函数,顾名思义,先去执行Before注解下的函数,再去执行Test注解下的函数,同时如果有多个Test函数,需要依次运行,那么studentServiceImpl对象初始化一次就可以在多个Test函数里面公用,十分方便。
  • 最后经过测试,并对比控制台的输出,发现使用这种方式不需要加载Spring的上下文环境, 而且速度很快,大约 0.6秒 ,不到一秒钟

例子3

到这里,可能有小伙伴就要问了,StudentServiceImpl里面如果也有@Resource依赖注入的bean呢?这在实际开发环境中十分常见,类似于这样

@Component(value = "studentServiceImpl")
public class StudentServiceImpl implements StudentService {

	@Resource
	private StudentMapper studentMapper;

	@Override
	 public String getStudentName(){
		return studentMapper.getStudentName();
	}
}
@Service
public class StudentMapper {

   public String getStudentName(){
      return "张三";
   }
}

此处我把getStudentName函数放到了StudentMapper类中,一般来说Mapper类与MyBatis结合使用,用于定义查询数据接口,此处为了方便起见,暂时这么用了,后续代表Mock数据库查询的时候也会使用到。

如果此时,我们再去运行测试类,就会出现这个错误:

image-20220904010055508

提升我们在测试环境中找不到StudentMapper这个Bean,为空指针。此时我们可以通过构造函数的方式,将StudentMapper注入进去,直接上代码

@Component(value = "studentServiceImpl")
public class StudentServiceImpl implements StudentService {

   private StudentMapper studentMapper;

   @Autowired //将Autowired作用域构造函数上
   public StudentServiceImpl(StudentMapper studentMapper) {
      this.studentMapper = studentMapper;
   }

   @Override
    public String getStudentName(){
      return studentMapper.getStudentName();
   }
}
public class StudentTest {

   private StudentServiceImpl studentServiceImpl;

   private StudentMapper studentMapper;

   @Before
   public void setBean(){
       //通过new初始化对象再进行调用
      this.studentMapper=new StudentMapper(); 
      this.studentServiceImpl=new StudentServiceImpl(this.studentMapper);
   }

   @Test
   public void getStudentName() {
      System.out.println(this.studentServiceImpl.getStudentName());
   }
}

一般来说@Autowired注解作用在类上,表示获取bean,而此处作用在StudentServiceImpl构造函数中。首先我们要知道,在Spring中如果要生成一个bean首先要进行初始化操作,去执行构造函数,那么如果有多个构造函数,Spring又会怎么选择呢?答案就是 :Spring中构造方法调用优先级为:带@Autowired的有参构造方法 > 不带@Autowired的有参构造方法 > 无参构造方法,所以当Spring检测到带Autowird的有参构造函数A时,观察参数是什么,如果参数B还是一个想要交给Spring管理的bean(即通过@Component等注解标识),那么Spring先去生成B参数的bean,等生成完了,在回来去生成A的bean,那么此时就不会有空指针的错误了。那么在单元测试用例中,我们只需要先将B new出来,在交给A处理就完事了。so easy。

此时可能又有小伙伴问了,如果Mapper类真的是数据库操作连接怎么办,或者是一些网络请求怎么办,这时候官方就建议我们使用Mock对象去模拟了。关于Mock对象具体怎么使用,我会在专门记录一下。

参考连接:

  1. https://segmentfault.com/a/1190000040153568
  2. https://juejin.cn/post/6918758473131884558
  3. https://blog.csdn.net/qq_41426990/article/details/119928396
  4. Testing (spring.io)
posted @ 2022-09-04 01:22  Dream可乐  阅读(213)  评论(0)    收藏  举报