1、什么是mock测试
1.png
Mock测试就是在测试活动中;对于某些不容易构造或者不容易获取的比较复杂的数据/场景;用一个虚拟的对象(Mock对象)来创建用于测试的测试方法。
2、为什么要进行Mock测试
Mock是为了解决不同的单元之间由于耦合而难于开发、测试的问题。所以;Mock既能出现在单元测试中;也会出现在集成测试、系统测试过程中。
Mock最大的功能是帮你把单元测试的耦合分解开;如果你的代码对另一个类或者接口有依赖;它能够帮你模拟这些依赖;并帮你验证所调用的依赖的行为。
3、Mock适用场景
1、需要将当前被测单元和其依赖模块独立开来;构造一个独立的测试环境;不关注被测单元的依赖对象;只关注被测单元的功能逻辑。
2、被测单元依赖的模块尚未开发完成;而被测单元需要依赖模块的返回值进行后续处理。
3、前后端项目中;后端接口开发完成之前;接口联调
4、依赖的上游项目的接口尚未开发完成;需要接口联调测试
5、被测单元依赖的对象较难模拟或者构造比较复杂
如: 支付业务的异常条件很多;但是模拟这种异常条件很复杂或者无法模拟.
4、代码实例
新建测试工程
package com.echo.mockito; public class demo { //新建一个测试方法 public int add(int a, int b){ return a ; b; } }
构建mock测试方法
选中测试类;右键选中generate
2.png
点击test
3.png
点击ok后就会在test目录下生成对应的测试方法;和真实的目录是对应的
4.png
5、参数方法说明
;BeforeEach
用在测试前准备;测试前会构建很多的环境配置或者基础配置;都可以在这里设置。
;AfterEach
用于测试后设置。
;Mock
注解可以理解为对 mock 方法的一个替代;不会走真实的方法;模拟真实方法的行为。使用该注解时;要使用MockitoAnnotations.openMocks(this) 方法;让注解生效。
;Spy
1、被Spy的对象会走真实的方法;而mock对象不会;
2、spy方法的参数是对象实例;mock的参数是class。
;InjectMocks
用于将;Mock标记的模拟变量注入到测试类中。
MockitoAnnotations.openMocks(this)
开启mock;配合以上两个注解进行测试。一般放在;BeforeEach 中;在测试前开启;这样不用在每个方法中都的开启了。
Mockito.when(demo.add(1,2)).thenReturn(3);打桩
mock核心;可以设置要测试的方法的结果;这样就会忽略真实方法的执行结果;后续的测试都是基于打桩结果执行。
Mockito.when(demo.add(1,2)).thenThrow(new RuntimeException());
用于模拟异常。
Assertions.assertEquals(3,demo.add(1,2));断言
测试的主要方式;结果基于此判断。(期望值;实际值)。
打桩测试 设置方法返回4 ;而实际执行为3 ;测试不通过。
5.png
不打桩测试 因为是spy方式;会走真实的方法 ;测试通过。
6.png
如果是mock方式;如果不打桩会有默认值;测试会不通过;可以试一下。
7.png
7、测试方法说明
详细调用看代码;一般会以下几种;
打桩测试
异常测试
真实方法调用
package com.echo.mockito; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.Spy; class demoTest { ;Mock demo demo; //测试前开启mock ;BeforeEach void setUp() { MockitoAnnotations.openMocks(this); } ;Test void add() { //mock 打桩;就是不管真实的方法如何执行;我们可以自行假设该方法执行的结果 //后续的测试都是基于打桩结果来走 // Mockito.when(demo.add(1,2)).thenReturn(4); // Assertions.assertEquals(3,demo.add(1,2)); //当测试方法出现异常;测试方法 如果有try{}catch{} 则可以测试异常是否正常 //Mockito.when(demo.add(1,1)).thenThrow(new RuntimeException()); //调用真实的方法 Mockito.when(demo.add(1,1)).thenCallRealMethod(); Assertions.assertEquals(2,demo.add(1,1)); } ;AfterEach void after(){ System.out.println(;测试结束;); } }
8、Mock静态方法
之前的版本中是不允许模拟测试静态方法的;如果需要测试静态方法;需要替换新的mock依赖;注释掉目前有的依赖;会有冲突。
静态方法测试要用mock的MockedStatic类构建测试方法。
<!-- <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>4.6.1</version> </dependency> --> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-inline</artifactId> <version>4.3.1</version> <scope>test</scope> </dependency>
package com.echo.mockito.Util; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; import org.mockito.Mockito; import java.util.Arrays; import static org.junit.jupiter.api.Assertions.*; class StaticUtilsTest { ;BeforeEach void setUp() { } // 有参静态方法构建 ;Test void range() { MockedStatic demo = Mockito.mockStatic(StaticUtils.class); //打桩 demo.when(()->StaticUtils.range(2,6)).thenReturn(Arrays.asList(10,11,12)); Assertions.assertTrue(StaticUtils.range(2,6).contains(11)); } // 无参静态方法构建 ;Test void name() { MockedStatic demo = Mockito.mockStatic(StaticUtils.class); //打桩 demo.when(StaticUtils::name).thenReturn(;dhmw;); Assertions.assertEquals(;dhmw;,StaticUtils.name()); } }
问题;单个的方法执行是没有问题的;但是我们在类上全部执行的时候;发现报错。
提示static mocking is already registered in the current thread To create a new mock, the existing static mock registration must be deregistered
意思就是说;每个方法需要有自己的static mock 对象;不允许公用。一起执行的时候;第一个方法占了对象;第二个方法就没有办法再占了。
8.png
解决;每个方法执行完毕后就直接关闭mock对象 demo.close()。相当于是单例的。用完后就的释放;下一个方法才能接着使用。
;Test void range() { MockedStatic demo = Mockito.mockStatic(StaticUtils.class); //打桩 demo.when(()->StaticUtils.range(2,6)).thenReturn(Arrays.asList(10,11,12)); Assertions.assertTrue(StaticUtils.range(2,6).contains(11)); //关闭 demo.close(); } // 无参静态方法构建 ;Test void name() { MockedStatic demo = Mockito.mockStatic(StaticUtils.class); //打桩 demo.when(StaticUtils::name).thenReturn(;dhmw;); Assertions.assertEquals(;dhmw;,StaticUtils.name()); //关闭 demo.close(); }
9、提升测试覆盖率
案例;数据统计系统;地推人员输入客户的姓名和手机号码;最后构建用户对象存入数据表。
业务代码如下;
package com.echo.mockito.service.impl; import com.echo.mockito.dao.UserDao; import com.echo.mockito.service.RegistrationService; import com.echo.mockito.vo.User; import org.springFramework.beans.factory.annotation.Autowired; import javax.xml.bind.ValidationException; import java.sql.SQLException; public class RegistrationServiceImpl implements RegistrationService { ;Autowired UserDao userDao; ;Override public User register(String name, String phone) throws Exception { if (name == null || name.length() == 0){ throw new ValidationException(;name 不能为空;); } if (phone == null || phone.length() ==0 ){ throw new ValidationException(;phone 不能为空;); } User user; try { user = userDao.save(name,phone); }catch (Exception e){ throw new Exception(;SqlException thrown; ; e.getMessage()); } return user; } }
package com.echo.mockito.dao; import com.echo.mockito.vo.User; public class UserDao { public User save(String name,String phnoe){ User user = new User(); user.setName(name); return user; } }
生成相应的测试代码;此时有几个问题需要思考。
1、测试的类是RegistrationServiceImpl但是里面的userDao如何注入到测试类中?
2、因为需要测试覆盖度;需要考虑该测试类中总共有几种情况需要测试。
;1;;两个if抛出了两个异常;总共是2种情况要测试。
;2;;保存数据库分为正常保存和异常保存;总共2种情况测试。
综上所述;必须完成这四种情况的测试;才能覆盖所有代码。
相同的方法;我们生成测试用例.代码中有详细的说明;每一种情况都有测试用例。
package com.echo.mockito.service.impl; import com.echo.mockito.dao.UserDao; import com.echo.mockito.vo.User; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.*; import javax.xml.bind.ValidationException; import java.sql.SQLException; import static org.junit.jupiter.api.Assertions.*; class RegistrationServiceImplTest { ;InjectMocks //RegistrationServiceImpl 实例中注入;Mock标记的类;此处是注入userDao ;Spy private RegistrationServiceImpl registrationService; ;Mock private UserDao userDao; ;BeforeEach void setUp() { MockitoAnnotations.openMocks(this); } ;Test void register() throws Exception { // ------------------ 第一种 name 异常情况 测试 start ------------------------ String name = null; String phone = ;1234;; try { registrationService.register(name,phone); }catch (Exception e){ Assertions.assertTrue(e instanceof ValidationException); } // ------------------ name 异常情况 测试 end ------------------------ // ------------------ 第二种 phone 异常情况 测试 start ------------------------ name = ;111;; phone = null; try { registrationService.register(name,phone); }catch (Exception e){ Assertions.assertTrue(e instanceof ValidationException); } // ------------------ phone 异常情况 测试 start ------------------------ // ------------------ 第三种 userDao.save 正常情况 测试 start ------------------------ name = ;111;; phone = ;111;; //正常保存测试 打桩 走真实的方法 Mockito.when(userDao.save(name,phone)).thenCallRealMethod(); User user = registrationService.register(name,phone); Assertions.assertEquals(;111;,user.getName()); // ------------------ userDao.save 正常情况 测试 end ------------------------ // ------------------ 第四种 userDao.save 异常情况 测试 start ------------------------ //异常保存测试 打桩 通过thenThrow 抛出异常 测试异常是否被捕获 Mockito.when(userDao.save(name,phone)).thenThrow(new RuntimeException()); try { registrationService.register(name,phone); }catch (Exception e){ Assertions.assertTrue(e instanceof Exception); } // ------------------ userDao.save 异常情况 测试 end ------------------------ } }
如上所示;上面提到的第一个问题;如何注入测试类中的成员变量;是通过;InjectMocks 注解即可完成。
测试覆盖率方法如下;
9.png
测试结果如下;
1、右边部分会显示测试覆盖率。
2、真实代码绿色代表已覆盖测试;红色代表未覆盖测试。
11.png
如果所有的测试情况都100%覆盖;结果如下;
12.png
综上所述就是覆盖测试的方法;总结如下;
1、根据业务代码;分析出所有需要测试的情况。
2、根据不同的测试情况;编写具体的测试代码。
3、针对每一种情况;可以编写具体的测试代码;然后通过打桩;断言等方式;穷尽所有的清册情况即可。
问题;
1、如果真实代码 方法级别有 throws Exception 那么同样的;测试方法也必须方法级别要抛出异常;不然测试会报错。
;Test void register() throws Exception {
2、保存数据库分为正常和异常;那么先测正常分支;然后在测试异常分支;如果顺序反了;测试先抛出异常;正常的分支就不会执行;这样会导致测试覆盖不全。
最后感谢每一个认真阅读我文章的人;看着粉丝一路的上涨和关注;礼尚往来总是要有的;虽然不是什么很值钱的东西;如果你用得到的话可以直接拿走
这些资料;对于做【软件测试】的朋友来说应该是最全面最完整的备战仓库;这个仓库也陪伴我走过了最艰难的路程;希望也能帮助到你;凡事要趁早;特别是技术行业;一定要提升技术功底。希望对大家有所帮助…….