超详细 springboot 整合 Mock 进行单元测试!本文带你搞清楚!

什么是Mock

1、Mock定义

Mockito是Java单元测试开发框架。在写测试单元时它可以Mock(Mock的中文释义是模拟,所以Mockito从名字上可以看出是要模拟一种场景)。

它可以模拟任何 Spring 管理的 Bean、模拟方法的返回值、模拟抛出异常等,避免为了测试一个方法,却要自行构建整个bean的依赖链。

Mock 测试主要是用来进行开发中一些未完成的接口或者网络断开、数据库连接错误等方法调用。

举个例子:

如下代码所示,list 集合需要从数据库查询出来。但是如果暂时数据库不能用,有需要测试,这个时候就可以进行 Mock 模拟出符合条件的 list 集合进行本地测试,无需连接数据库。

2、为什么使用

在对代码进行单元测试过程中,经常会有以下的情况发生:

 
  1. class A 依赖 class B

  2. class B 依赖 class C和class D

  3. class C 依赖 ...

  4. class D 依赖 ...

  5. 1.被测对象依赖的对象构造复杂

  6. 我们想对class A进行单元测试,需要构造大量的class B、C、D等依赖造步骤多、耗时较长的对象,

  7. 对于他们的构造我们可以利用mock去构造过程复杂的对象用于class A的测试,

  8. 因为我们只是想测试class A的行为是否符合预期,我们并不需要测试依赖对象。

  9. 2.被测单元依赖的模块尚未开发完成,而被测对象需要依赖模块的返回值进行测试:

  10. ----- 比如service层的代码中,包含对dao层的调用,但dao层代码尚未实现

  11. ----- 比如web的前端依赖后端接口获取数据进行联调测试,但后端接口并未开发完成

  12. ----- 比如数据库还不能正常使用但是需要测试功能逻辑是否可行。

3、常用的Mock技术

  • PowerMock

  • EasyMock

  • Mockito

  • JMock

目前在 Java 中主流的 Mock 测试工具有 Mockito、JMock、EasyMock 等等,而 SpringBoot 目前内建的是 Mockito 框架

4、Mokito中文文档

Mokito中文官网:https://github.com/hehonghui/mockito-doc-zh#0

5、集成测试和单元测试区别

  • (1)集成测试

测试过程中,会启动整个Spring容器,调用DB 或者 依赖的外部接口等。只不过访问的环境是测试环境。这个过程最大程度还原生产环境过程,但是耗时长。

  • (2)单元测试

不启动整个应用,只对单个接口/类进行测试。不调用DB 、外部接口,依赖的服务都Mock掉,只测试代码逻辑。这个过程,测试用例耗时短。

API

1、Mockito的API

  • mock:构建一个我们需要的对象;可以mock具体的对象,也可以mock接口

  • spy:构建监控对象

  • verify:验证某种行为

  • when:当执行什么操作的时候,一般配合thenXXX 一起使用。表示执行了一个操作之后产生什么效果

  • doReturn:返回什么结果

  • doThrow:抛出一个指定异常

  • doAnswer:做一个什么相应,需要我们自定义Answer

  • times:某个操作执行了多少次

  • atLeastOnce:某个操作至少执行一次

  • atLeast:某个操作至少执行指定次数

  • atMost:某个操作至多执行指定次数

  • atMostOnce:某个操作至多执行一次

  • doNothing:不做任何处理

  • doReturn:返回一个结果

  • doThrow:抛出一个指定异常

  • doAnswer:指定一个操作,传入Answer

  • doCallRealMethod:返回真实业务执行的结果,只能用于监控对象

2、ArgumentMatchers参数匹配

  • anyInt:任何int类型的参数,类似的还有anyLong/anyByte等等。

  • eq:等于某个值的时候,如果是对象类型的,则看toString方法

  • isA:匹配某种类型

  • matches:使用正则表达式进行匹配

3、OngoingStubbing返回操作

  • thenReturn:指定一个返回的值

  • thenThrow:抛出一个指定异常

  • then:指定一个操作,需要传入自定义Answer

  • thenCallRealMethod:返回真实业务执行的结果,只能用于监控对象

Mockito的使用

1、添加Maven依赖

  • Java 环境依赖

 
  1. <!-- https://mvnrepository.com/search?q=Mockito-->

  2. <dependency>

  3. <groupId>org.mockito</groupId>

  4. <artifactId>mockito-core</artifactId>

  5. <version>4.4.0</version>

  6. </dependency>

  • SpringBoot 环境依赖

注意:SpringBoot 默认的 Mock 框架是 Mockito,和 junit 一样,只需要依赖 spring-boot-starter-test 就可以了。

 
  1. <dependency>

  2. <groupId>org.springframework.boot</groupId>

  3. <artifactId>spring-boot-starter-test</artifactId>

  4. </dependency>

2、@InjectMocks、@Mock使用

  • @Mock: 用于代替Mockito.mock创建mock对象,创建一个Mock实例,需要基于JUnit5环境。

  • @InjectMocks: 创建一个实例,其余用@Mock(或@Spy)注解创建的mock将被注入到用该实例中。

直白的理解就是:

如图,实体类TUserServiceImpl通过注解 @Autowired注入了三个实体:TUserMapper、JdbcTemplate 和 NamedParameterJdbcTemplate。

如果想要测试TUserServiceImpl,那么test中,TUserMapper、JdbcTemplate 和 NamedParameterJdbcTemplate就要用@Mock注解创建Mock实例,而要被测试TUserServiceImpl就要用@InjectMocks注解创建实例,这个时候被@Mock注解创建的TUserMapper、JdbcTemplate 和 NamedParameterJdbcTemplate就被注入到通过 @InjectMocks注解创建的TUserServiceImpl实例中。

如下图所示:

最后的大白话解释 !!!

你要测试哪个类(如TUserServiceImpl),那么就用 @InjectMocks注解;被测试的类中通过 @Autowired注解注入了几个,那么测试类里面就用@Mock注解创建几个实例!

使用Mockito的注解,需要让注解生效,让注解生效的方法有两个:

  • 1.给被测类添加 @RunWith(MockitoJUnitRunner.class) 或者 @RunWith(SpringJUnit4ClassRunner.class) 注解

 
  1. @RunWith(MockitoJUnitRunner.class)

  2. public class MockitoAnnotationTest {

  3. ...

  4. }

  • 2.在初始化方法中使用MockitoAnnotations.openMocks(this)

 
  1. @Before

  2. public void init() {

  3. MockitoAnnotations.openMocks(this);

  4. }

注意:新版spring-boot-starter-test不再集成junit,而是junit-jupiter,找不到@RunWith注解:

  1. spring-boot-starter-test 2.5.5 版本只需要在类上加上@SpringBootTest即可,不需要再加@RunWith()注解了。

  2. spring-boot-starter-test 2.4.x 版本的也没有@RunWith()注解,至于从哪个版本开始没有@RunWith()注解的,请自行查阅相关文档。

  3. 一些较低版本也没有 openMocks 方法,而是 initMocks。

3、SpringbootTest 注解和 RunWith 注解在测试类的作用

  • @SpringbootTest

这个注解相当于启动类的作用,加了这个注解后,当使用加了@Test注解的方法时,会加载Spring上下文,跟SpringbootApplication这个启动类一样,把bean加载进IOC容器。

其中参数classes 需指明启动类.class,如果不指明,需要保证启动类所在的包和加了SpringbootTest注解的类 在同一个包或者是启动类的子包下,否则注入到( @Autowired / @Resource)会报空指针异常。

如下:

@SpringBootTest(classes = MySpringbootApplication.class)
  • @RunWith

@RunWith(SpringRunner.class),作用是与Spring环境整合,因为在测试类中我们可以需要用@Autowired自动装配IOC容器中的bean,所以需要与Spring环境进行整合,才能实现自动装配,否则会装配失败,导致bean为null。

有时候会发现,有的测试类不添加@RunWith也能注入成功,这是因为,如果导入@Test注解的包是org.junit.jupiter.api.Test,则不需要添加@RunWith注解,如果导入的是org.junit.Test,则需要添加,这点需要注意。

Mock 测试代码案例

1、添加依赖

 
  1. <dependency>

  2. <groupId>org.springframework.boot</groupId>

  3. <artifactId>spring-boot-starter-test</artifactId>

  4. </dependency>

注意:本案例用的 springboot 版本是 2.6 版本

2、编写业务代码

 
  1. @Service

  2. public class PositionService {

  3. public String getStr(String str){

  4. return str;

  5. }

  6. public void getVoid(){

  7. System.out.println("没有返回值");

  8. }

  9. }

3、Mock 测试

(1)常规测试

先不使用 Mockito ,而是真的去调用一个正常的 Spring bean ,测试类写法如下。其实就是很普通的注入 PositionService bean,然后去调用他的方法。

测试代码:

注意:

  • 可以看到测试类中引用的包是 org.junit.jupiter.api.Test 和 org.junit.jupiter.api.BeforeEach,是 jupiter.api 包下面的,此时测试类只用了 @SpringBootTest 这一个注解;

  • 但是,如果用的是 org.junit.Test 和 org.junit.Before,测试类上面必须同时用 @RunWith(SpringRunner.class) 和 @SpringBootTest(classes = MySpringbootApplication.class)。必须同时用!!!!!

 
  1. import com.cyd.applicationstartup.MySpringbootApplication;

  2. import org.junit.jupiter.api.Test;

  3. import org.junit.jupiter.api.BeforeEach;

  4. import org.springframework.beans.factory.annotation.Autowired;

  5. import org.springframework.boot.test.context.SpringBootTest;

  6. @SpringBootTest(classes = MySpringbootApplication.class)

  7. public class PositionServiceTest {

  8. @Autowired

  9. private PositionService positionService;

  10. @BeforeEach

  11. public void init() {

  12. // 对所有注解了@Mock的对象进行模拟

  13. MockitoAnnotations.openMocks(this);

  14. System.out.println("初始化方法");

  15. }

  16. @Test

  17. public void testGetStr() {

  18. String str = positionService.getStr("刘亦菲");

  19. System.out.println("测试结果:" + str);

  20. }

  21. }

测试结果:

(2)Mock 测试

Mock 测试需要自定返回结果,结果和方法返回结果类型一致。

语法如下:

Mockito.when( 对象.方法名() ).thenReturn( 自定义结果 )

使用 Mockito 模拟 Bean 的单元测试代码示例如下:

 
  1. import org.junit.jupiter.api.Test;

  2. import org.junit.jupiter.api.BeforeEach;

  3. import org.mockito.Mock;

  4. import org.mockito.Mockito;

  5. import org.springframework.boot.test.context.SpringBootTest;

  6. @SpringBootTest(classes = MySpringbootApplication.class)

  7. public class PositionServiceTest {

  8. @Mock

  9. private PositionService positionService;

  10. @BeforeEach

  11. public void init() {

  12. System.out.println("初始化方法");

  13. }

  14. @Test

  15. public void testMockGetStr() {

  16. // 定义当调用mock positionService 的 getStr() 方法,并且任意字符串参数时,就返回字符串 "刘亦菲"

  17. Mockito.when(positionService.getStr(Mockito.anyString())).thenReturn("刘亦菲");

  18. // 定义当调用 mock positionService 的 getStr() 方法,并且参数是字符串 "美女" 时,就返回字符串 "刘亦菲"

  19. Mockito.when(positionService.getStr("美女")).thenReturn("刘亦菲");

  20. System.out.println(positionService.getStr("美女"));

  21. // 验证 positionService 的 getStr()这个方法是否被调用过

  22. Mockito.verify(positionService).getStr("刘亦菲");

  23. }

  24. }

注意:

  • 代码中第一个 Mockito.when 的参数用的是 Mockito.anyString(),表示 任意字符串参数调用 getStr() 方法,就会返回字符串 “刘亦菲”;

  • 第二个 Mockito.when 的参数用的是字符串"美女",表示限制只有当参数是 "美女"时,才会返回 “刘亦菲”。

  • 因此,在日常 Mock 测试中,通常使用 Mockito.any 作为参数。

(3)Mock 测试常用方法

thenReturn 系列方法

① 定义当调用mock positionService 的 getStr() 方法,并且任意字符串参数时,就返回字符串 “哈哈哈哈”:

Mockito.when(positionService.getStr(Mockito.anyString())).thenReturn("哈哈哈哈");System.out.println(positionService.getStr("任意参数"));

表示任意值的参数如下图:

② 定义当调用 mock positionService 的 getStr() 方法,并且限制参数只有是字符串 “美女” 时,才返回字符串 “刘亦菲”:

Mockito.when(positionService.getStr("美女")).thenReturn("刘亦菲");System.out.println(positionService.getStr("美女"));

thenThrow 系列方法

① 当调用 mock positionService 的 getStr() 方法,输入的的参数是 字符串 “9” 时,抛出一个 RuntimeException:

Mockito.when(positionService.getStr("9")).thenThrow(new RuntimeException("mock throw exception"));String str = positionService.getStr("9"); //会抛出一个RuntimeExceptio

测试结果:

② 如果方法没有返回值的话(即是方法定义为 public void myMethod() {…}),要改用 doThrow() 抛出 Exception:

Mockito.doThrow(new RuntimeException("mock throw exception when method is void")).when(positionService).getVoid();positionService.getVoid(); //会抛出一个RuntimeException

测试结果:

verify 系列方法

① 检查调用 positionService 的 getStr() 方法,、且参数为 “3” 的次数是否为1次:

Mockito.verify(positionService, Mockito.times(1)).getStr(Mockito.eq("3"));

② 验证调用顺序,验证 positionService 是否先调用 getStr() 两次,并且第一次的参数是 “3”、第二次的参数是 “5”,然后才调用 getVoid() 方法:

 
  1. InOrder inOrder = Mockito.inOrder(positionService);

  2. inOrder.verify(positionService).getStr("3");

  3. inOrder.verify(positionService).getStr("5");

  4. inOrder.verify(positionService).getVoid();

模拟对象有两种方式:

  • 对注解了 @Mock 的对象进行模拟MockitoAnnotations.openMocks(this);

  • 对单个对象手动 mock :xxx= Mockito.mock(xxx.class);

对 void 的方法设置模拟:

positionService 中有如下方法:

public void getVoidWithParam(String param){        System.out.println("没有返回值");    }

Mock 测试方法:

    /*对void的方法设置模拟*/        Mockito.doAnswer(invocationOnMock -> {            System.out.println("进入了Mock");            return null;        }).when(positionService).getVoidWithParam("param");

(4)Mock 测试常用注解

  • 全部 Mock

@Mockprivate ServiceA serviceA;

这种方式,serviceA中的所有方法都会被mock,并不会真正被调用到。

  • 依赖注入

ServiceA 依赖了 ServiceC 和 DaoA,使用InjectMocks可以自动注入。

@InjectMocksprivate ServiceA serviceA;
  • 真实调用

@Spyprivate ServiceC serviceC;

这种方式,调用serviceC的方法,会被真实调用。

Mock测试结合Java反射综合案例

 
  1. import java.lang.reflect.InvocationTargetException;

  2. import java.lang.reflect.Method;

  3. import java.math.BigDecimal;

  4. import java.text.SimpleDateFormat;

  5. import java.util.ArrayList;

  6. import java.util.Date;

  7. import org.junit.Before;

  8. import org.junit.Test;

  9. import org.junit.runner.RunWith;

  10. import org.mockito.InjectMocks;

  11. import org.mockito.Mock;

  12. import org.mockito.Mockito;

  13. import org.mockito.MockitoAnnotations;

  14. import org.mockito.runners.MockitoJUnitRunner;

  15. import org.springframework.test.util.ReflectionTestUtils;

  16. @RunWith(MockitoJUnitRunner.class)

  17. public class MyMockServiceTest {

  18. @Mock

  19. private CustomerDao customerDao;

  20. @Mock

  21. private MockDaoA mockDaoA;

  22. @Mock

  23. private MockDaoC mockDaoC;

  24. @Mock

  25. private MockDaoD mockDaoD;

  26. @Mock

  27. private MockDaoE mockDaoE;

  28. @InjectMocks

  29. MyMockService myMockService;

  30. MockTestDataDto mockTestDataDto;

  31. @Before

  32. public void init() {

  33. // Apollo 配置

  34. ReflectionTestUtils.setField(myMockService, "mockValue", "58699DFR-1456984524");

  35. MockitoAnnotations.initMocks(this);

  36. mockTestDataDto = new MockTestDataDto();

  37. mockTestDataDto.setCallback("callback");

  38. PolicyRelatedInfo policyRelatedInfo = new PolicyRelatedInfo();

  39. policyRelatedInfo.setRelationToAppnt("1");

  40. Mockito.when(mockDaoA.selectRelationByParams(Mockito.any()))

  41. .thenReturn(policyRelatedInfo);

  42. Customer customer = new Customer();

  43. insu.setPhone("4654");

  44. insu.setSex("1");

  45. insu.setIdType("1");

  46. insu.setIdNo("1");

  47. insu.setName("张三");

  48. insu.setBirthday(new Date());

  49. Mockito.when(customerDao.selectByPrimaryKey(Mockito.anyInt())).thenReturn(customer);

  50. }

  51. @Test

  52. public void test() throws NoSuchMethodException, SecurityException, IllegalAccessException,

  53. IllegalArgumentException, InvocationTargetException, InstantiationException {

  54. ArrayList<PolicyDanger> getPolicyDangerList = new ArrayList<>();

  55. PolicyDanger policyDanger1 = new PolicyDanger();

  56. policyDanger1.setIsPassFlag("M");

  57. policyDanger1.setDanderCode("595648FD");

  58. policyDanger1.setTotalAmnt(new BigDecimal(100.1223));

  59. PolicyDanger policyDanger2 = new PolicyDanger();

  60. policyDanger2.setIsPassFlag("M");

  61. policyDanger2.setDanderCode("595648FD");

  62. policyDanger2.setTotalAmnt(new BigDecimal(100.1223));

  63. getPolicyDangerList.add(policyDanger1);

  64. getPolicyDangerList.add(policyDanger2);

  65. Mockito.when(mockDaoC.selectPolicyDangerList(Mockito.any())).thenReturn(getPolicyDangerList);

  66. ArrayList<Province> provinceList = new ArrayList<>();

  67. Province province = new Province();

  68. province.setProvinceCode("5894");

  69. province.setDutyCode("5928D2");

  70. provinceList.add(province);

  71. Mockito.when(mockDaoD.selectPolicyByQueryParam(Mockito.any())).thenReturn(provinceList);

  72. ArrayList<User> userList = new ArrayList<>();

  73. User user = new User();

  74. user.setBuyDate(new Date());

  75. userList.add(user);

  76. Mockito.when(mockDaoE.selectUserByQueryParam(Mockito.any())).thenReturn(userList);

  77. // 反射获得类

  78. MyMockService hx = new MyMockService();

  79. Class<? extends MyMockService> cls1 = hx.getClass();

  80. // 通过指定方法名称和参数类型的方法来获取Method对象(注意: 如果方法名称不存在或参数类型不正确的话,会报错,不会返回null)

  81. // 注意:这里测试的是 private 修饰的私有方法,需要用 getDeclaredMethod

  82. // setUserInfo 是需要测试的方法名,后面为该方法需要的参数类型

  83. Method method = cls1.getDeclaredMethod("setUserInfo", MockTestDataDto.class, Integer.class, String.class,

  84. SimpleDateFormat.class);

  85. method.setAccessible(true);

  86. // 执行方法

  87. method.invoke(myMockService, mockTestDataDto, 1, "1", new SimpleDateFormat());

  88. }

  89. }

注意:

如果测试的类中有如下配置:

 @Value("${mock.mapping.name}") private String  mockValue;

测试代码中需要如下设置配置值:

 ReflectionTestUtils.setField(mockService, "mockValue", "58699DFR-1456984524");

 

感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:

这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!有需要的小伙伴可以点击下方小卡片领取   

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值