# Mockit测试案列 **Repository Path**: liu-yong-123/mockitceshianlie ## Basic Information - **Project Name**: Mockit测试案列 - **Description**: Mockito+MockMvc+Junit 学习案列 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 3 - **Created**: 2024-08-19 - **Last Updated**: 2024-08-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Mockit+MockMvc+Junit 解答 ## 前景知识 什么是 TDD,BDD,DDD? 1、TDD:测试驱动开发(Test-Driven Development) 测试驱动开发是敏捷开发中的一项核心实践和技术,也是一种设计方法论,TDD首先考虑使用需求(对象、功能、过程、接口等) 主要是编写测试用例框架对功能的过程和接口进行设计,而测试框架可以持续进行验证。大行其道的一些模式对TDD的支持都非常不错,比如MVC和MVP等 2、BDD:行为驱动开发(Behavior Driven Development) 也就是行为驱动开发。这里的B并非指的是Business,实际上BDD可以看作是对TDD的一种补充,让开发、测试、BA以及客户都能在这个基础上达成一致,JBehave之类的BDD框架 3、ATDD:验收测试驱动开发(Acceptance Test Driven Development) 通过单元测试用例来驱动功能代码的实现,团队需要定义出期望的质量标准和验收细则,以明确而且达成共识的验收测试计划(包含一系列测试场景)来驱动开发人员的TDD实践和测试人员的测试脚本开发。面向开发人员,强调如何实现系统以及如何检验 4、DDD:领域驱动开发(Domain Drive Design) DDD指的是Domain Drive Design,也就是领域驱动开发,DDD实际上也是建立在这个基础之上,因为它关注的是Service层的设计,着重于业务的实现,将分析和设计结合起来,不再使他们处于分裂的状态,这对于我们正确完整的实现客户的需求,以及建立一个具有业务伸缩性的模型 ## Junit ### Junit 基本注解介绍 - `@BeforeClass` 在所有测试方法执行前执行一次,一般在其中写上整体初始化的代码。 - `@AfterClass` 在所有测试方法后执行一次,一般在其中写上销毁和释放资源的代码。 ```java // 注意这两个都是静态方法 @BeforeClass public static void test(){ } @AfterClass public static void test(){ } ``` - `@Before` 在每个方法测试前执行,一般用来初始化方法(比如我们在测试别的方法时,类中与其他测试方法共享的值已经被改变,为了保证测试结果的有效性,我们会在@Before注解的方法中重置数据) - `@After` 在每个测试方法执行后,在方法执行完成后要做的事情。 - `@Test(timeout = 1000)` 测试方法执行超过1000毫秒后算超时,测试将失败。 - `@Test(expected = Exception.class)` 测试方法期望得到的异常类,如果方法执行没有抛出指定的异常,则测试失败。 - `@Ignore("not ready yet")` 执行测试时将忽略掉此方法,如果用于修饰类,则忽略整个类。 - `@Test` 编写一般测试用例用。 - `@RunWith` 在 Junit 中有很多个 Runner,他们负责调用你的测试代码,每一个 Runner 都有各自的特殊功能,你根据需要选择不同的 Runner 来运行你的测试代码。 如果我们只是简单的做普通 Java 测试,不涉及 Spring Web 项目,你可以省略 `@RunWith` 注解,你要根据需要选择不同的 Runner 来运行你的测试代码。 ### 测试方法执行顺序 按照设计,Junit不指定test方法的执行顺序。 - `@FixMethodOrder(MethodSorters.JVM)`:保留测试方法的执行顺序为JVM返回的顺序。每次测试的执行顺序有可能会所不同。 - `@FixMethodOrder(MethodSorters.NAME_ASCENDING`) :根据测试方法的方法名排序,按照词典排序规则(ASC,从小到大,递增)。 Failure 是测试失败,Error 是程序出错。 ## 什么是 Mock 测试 Mock 测试就是在测试过程中,对于某些不容易构造(如 HttpServletRequest 必须在Servlet 容器中才能构造出来)或者不容易获取比较复杂的对象(如 JDBC 中的ResultSet 对象),用一个虚拟的对象(Mock 对象)来创建以便测试的测试方法。 Mock 最大的功能是帮你把单元测试的耦合分解开,如果你的代码对另一个类或者接口有依赖,它能够帮你模拟这些依赖,并帮你验证所调用的依赖的行为。 比如一段代码有这样的依赖: 当我们需要测试A类的时候,如果没有 Mock,则我们需要把整个依赖树都构建出来,而使用 Mock 的话就可以将结构分解开,像下面这样: ### Mock 对象使用范畴 真实对象具有不可确定的行为,产生不可预测的效果(如:股票行情,天气预报) : - 真实对象很难被创建的 - 真实对象的某些行为很难被触发 - 真实对象实际上还不存在的(和其他开发小组或者和新的硬件打交道)等等 ### 使用 Mock 对象测试的关键步骤 - 使用一个接口来描述这个对象 - 在产品代码中实现这个接口 - 在测试代码中实现这个接口 - 在被测试代码中只是通过接口来引用对象,所以它不知道这个引用的对象是真实对象,还是 Mock 对象。 ### Mock 与 Stub 的区别 Mock 不是 Stub,两者是有区别的: - 前者被称为 mockist TDD,而后者一般称为 classic TDD ; - 前者是基于行为的验证(behavior verification),后者是基于状态的验证 (state verification); - 前者使用的是模拟的对象,而后者使用的是真实的对象。 ### Java Mock 测试 目前,在 Java 阵营中主要的 Mock 测试工具有 [Mockito](http://mockito.org/),[JMock](http://www.jmock.org/),[EasyMock](http://easymock.org/) 等。 测试用例的一般简要的三大元素 **输入条件 预期结果 实际结果** **期望** **运行** **验证** ==Mockito 关注的是 setub 和 验证== ## Mockito ### 特点 Mockito 是美味的 Java 单元测试 Mock 框架,[开源](https://github.com/mockito)。 大多 Java Mock 库如 EasyMock 或 JMock 都是 expect-run-verify (期望-运行-验证)方式,而 Mockito 则使用更简单,更直观的方法:在执行后的互动中提问。使用 Mockito,你可以[验证任何你想要的](http://monkeyisland.pl/2008/02/24/can-i-test-what-i-want-please)。而那些使用 expect-run-verify 方式的库,你常常被迫查看无关的交互。 [非 expect-run-verify 方式](http://monkeyisland.pl/2008/02/01/deathwish/) 也意味着,Mockito 无需准备昂贵的前期启动。他们的目标是透明的,让开发人员专注于测试选定的行为。 **Mockito 拥有的非常少的 API,所有开始使用 Mockito,几乎没有时间成本。因为只有一种创造 mock 的方式。只要记住,在执行前 stub,而后在交互中验证。你很快就会发现这样 TDD java 代码是多么自然。** ==[类似 EasyMock 的语法](https://github.com/mockito/mockito/wiki/Mockito-vs-EasyMock)来的,所以你可以放心地重构。Mockito 并不需要“expectation(期望)”的概念。只有 stub 和验证。== Mockito 实现了 [Gerard Meszaros](http://xunitpatterns.com/Mocks, Fakes, Stubs and Dummies.html) 所谓的 Test Spy. **这样Mockit框架的测试用例就具有如下特点** **==使用Mockito一般分三个步骤:1、模拟测试类所需的外部依赖;2、执行测试代码;3、判断执行结果是否达到预期;==** 一个测试方法主要包括三部分: 1)setup 2)执行操作 3)验证结果 ### 其他的一些特点: - 可以 mock 具体类而不单止是接口 - 一点注解语法糖 - `@Mock` - 干净的验证错误是 - 点击堆栈跟踪,看看在测试中的失败验证;点击异常的原因来导航到代码中的实际互动。堆栈跟踪总是干干净净。 - 允许灵活有序的验证(例如:你任意有序 `verify`,而不是每一个单独的交互) - 支持“详细的用户号码的时间”以及“至少一次”验证 - 灵活的验证或使用参数匹配器的 stub (`anyObject()`,`anyString()` 或 `refEq()` 用于基于反射的相等匹配) - 允许创建[自定义的参数匹配器](https://mockito.googlecode.com/svn/branches/1.6/javadoc/org/mockito/Matchers.html)或者使用现有的 hamcrest 匹配器 这篇文章讲的比较全 https://www.cnblogs.com/bodhitree/p/9456515.html SpringBoot 中的 `pom.xml` 文件需要添加的依赖: ```xml org.springframework.boot spring-boot-starter-test test ``` 进入 `spring-boot-starter-test-2.1.3.RELEASE.pom` 可以看到该依赖中已经有单元测试所需的大部分依赖,如: - junit - mockito - hamcrest 若为其他 spring 项目,需要自己添加 Junit 和 mockito 项目。 ### 常用的Mockito方法 | **方法名** | **描述** | | ------------------------------------------------------------ | --------------------------------------------- | | **Mockito.mock(classToMock)** | **模拟对象** | | **Mockito.verify(mock)** | **验证行为是否发生** | | **Mockito.when(methodCall).thenReturn(value1).thenReturn(value2)** | **触发时第一次返回value1,第n次都返回value2** | | **Mockito.doThrow(toBeThrown).when(mock).[method]** | **模拟抛出异常。** | | **Mockito.mock(classToMock,defaultAnswer)** | **使用默认Answer模拟对象** | | **Mockito.when(methodCall).thenReturn(value)** | **参数匹配** | | **Mockito.doReturn(toBeReturned).when(mock).[method]** | **参数匹配(直接执行不判断)** | | **Mockito.when(methodCall).thenAnswer(answer))** | **预期回调接口生成期望值** | | **Mockito.doAnswer(answer).when(methodCall).[method]** | **预期回调接口生成期望值(直接执行不判断)** | | **Mockito.spy(Object)** | **用spy监控真实对象,设置真实对象行为** | | **Mockito.doNothing().when(mock).[method]** | **不做任何返回** | | **Mockito.doCallRealMethod().when(mock).[method]
//等价于
Mockito.when(mock.[method]).thenCallRealMethod();** | **调用真实的方法** | | **reset(mock)** | **重置mock** | **代码示范** 1 验证行为是否发生 ```java package com.codeyang.mocktest2.mockit; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import java.io.IOException; import java.io.OutputStream; import java.util.Iterator; import java.util.List; import static org.mockito.Mockito.mock; /** * 描述: describe * * @author shengyang5 * @version 2021/7/22 11:51 */ @RunWith(MockitoJUnitRunner.class) public class MockitStudy01 { /** *1 验证行为是否发生 * * @throws Exception */ @Test public void test() throws Exception { //模拟创建一个List对象 List mock = mock(List.class); //调用mock对象的方法 mock.add(1); mock.clear(); //验证方法是否执行 Mockito.verify(mock).add(1); Mockito.verify(mock).clear(); } } ``` 2 多次触发返回不同值 ```java /** * 2 多次触发返回不同值 */ @Test public void test2() throws Exception { //mock一个Iterator类 Iterator mock = mock(Iterator.class); //预设当iterator调用next()时第一次返回hello,第n次都返回world //Mockito.when(methodCall).thenReturn(value1).thenReturn(value2) 触发时第一次返回value1,第n次都返回value2 Mockito.when(mock.next()).thenReturn("hello").thenReturn("world"); //使用mock的对象 String result = mock.next() + " " + mock.next() + " " + mock.next(); //验证结果 Assert.assertEquals("hello world world", result); } ``` 3模拟抛出异常 ```java /** * 3模拟抛出异常 */ @Test(expected = IOException.class)//期望报IO异常 public void when_thenThrow() throws IOException { OutputStream mock = Mockito.mock(OutputStream.class); //预设当流关闭时抛出异常 Mockito.doThrow(new IOException()).when(mock).close(); mock.close(); } ``` 4 使用默认Answer模拟对象 ```java /** * 4 使用默认Answer模拟对象 * RETURNS_DEEP_STUBS 是创建mock对象时的备选参数之一 * 以下方法deepstubsTest和deepstubsTest2是等价的 */ @Test public void deepstubsTest() { A a = Mockito.mock(A.class, Mockito.RETURNS_DEEP_STUBS); Mockito.when(a.getB().getName()).thenReturn("Beijing"); Assert.assertEquals("Beijing", a.getB().getName()); } @Test public void deepstubsTest2() { A a = Mockito.mock(A.class); B b = Mockito.mock(B.class); System.out.println(b); Mockito.when(a.getB()).thenReturn(b); Mockito.when(b.getName()).thenReturn("Beijing"); Assert.assertEquals("Beijing", a.getB().getName()); } class A { private B b; public B getB() { return b; } public void setB(B b) { this.b = b; } } class B { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSex(Integer sex) { if (sex == 1) { return "man"; } else { return "woman"; } } } ``` 5 参数匹配 ```java /** * 5 参数匹配 */ @Test public void with_arguments(){ B b = Mockito.mock(B.class); //预设根据不同的参数返回不同的结果 Mockito.when(b.getSex(1)).thenReturn("男"); Mockito.when(b.getSex(2)).thenReturn("女"); Assert.assertEquals("男", b.getSex(1)); Assert.assertEquals("女", b.getSex(2)); //对于没有预设的情况会返回默认值 Assert.assertEquals(null, b.getSex(0)); } class B{ private String name; public String getName(){ return name; } public void setName(String name){ this.name = name; } public String getSex(Integer sex){ if(sex==1){ return "man"; }else{ return "woman"; } } } ``` 6 匹配任意参数 `Mockito.anyInt()` 任何 int 值 ; `Mockito.anyLong()` 任何 long 值 ; `Mockito.anyString()` 任何 String 值 ; `Mockito.any(XXX.class)` 任何 XXX 类型的值 等等 ```java /** * 6 匹配任意参数 * * @throws Exception */ @Test public void test() throws Exception { //1模拟对象 List list = mock(List.class); //2匹配任意参数 Mockito.when(list.get(1)).thenReturn(1); // Mockito.argThat(new IsValid())) 使用自己的参数匹配器 自定义匹配规则 Mockito.when(list.contains(Mockito.argThat(new IsValid()))).thenReturn(true); Assert.assertEquals(1, list.get(1)); Assert.assertTrue(list.contains(1)); } /** * 7 自定义的参数匹配其 */ class IsValid implements ArgumentMatcher { @Override public boolean matches(Object argument) { if (argument instanceof Integer) { Integer integer = Integer.valueOf(argument.toString()); if (integer.equals(1)) { return true; } } System.out.println(argument); return false; } } ``` *注意:使用了参数匹配,那么所有的参数都必须通过matchers来匹配* Mockito继承Matchers,anyInt()等均为Matchers方法 当传入两个参数,其中一个参数采用任意参数时,指定参数需要matchers来对比 8自定义参数匹配 ```java /** * 8-自定义参数匹配 */ @Test public void argumentMatchersTest() { //创建mock对象 List mock = mock(List.class); //argThat(Matches matcher)方法用来应用自定义的规则,可以传入任何实现Matcher接口的实现类。 Mockito.when(mock.addAll(Mockito.argThat(new IsListofTwoElements()))).thenReturn(true); Assert.assertTrue(mock.addAll(Arrays.asList("one", "two", "three"))); } class IsListofTwoElements implements ArgumentMatcher { @Override public boolean matches(List argument) { return argument.size() == 3; } } ``` 9预期回调接口生成期望值 ```java //9预期回调接口生成期望值 @Test public void answerTest() throws Exception { List mockList = mock(List.class); //使用方法预期回调接口生成期望值(Answer结构) Mockito.when(mockList.get(Mockito.anyInt())).thenAnswer(new CustomAnswer()); Assert.assertEquals("hello world:0", mockList.get(0)); Assert.assertEquals("hello world:999", mockList.get(999)); } /** * 自定义的接口回复类实现 Answer接口 */ class CustomAnswer implements Answer { @Override public String answer(InvocationOnMock invocation) throws Exception { Object[] args = invocation.getArguments(); return "hello world:" + args[0]; } } // 等价于:(也可使用匿名内部类实现) @Test public void answer_with_callback(){ //使用Answer来生成我们我们期望的返回 Mockito.when(mockList.get(Mockito.anyInt())).thenAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { Object[] args = invocation.getArguments(); return "hello world:"+args[0]; } }); Assert.assertEquals("hello world:0",mockList.get(0)); Assert. assertEquals("hello world:999",mockList.get(999)); } ``` 10预期回调接口生成期望值(直接执行) ```java /** * 10预期回调接口生成期望值(直接执行) */ @Test public void testAnswer1() { List mock = Mockito.mock(List.class); Mockito.doAnswer(new CustomAnswer2()).when(mock).get(Mockito.anyInt()); Assert.assertEquals("大于三", mock.get(4)); Assert.assertEquals("小于三", mock.get(2)); //TDD 输出预期改变值 System.out.println(mock.get(2)); } public class CustomAnswer2 implements Answer { public String answer(InvocationOnMock invocation) throws Throwable { Object[] args = invocation.getArguments(); Integer num = (Integer) args[0]; if (num > 3) { return "大于三"; } else { return "小于三"; } } } ``` 11 修改对未预设的调用返回默认期望(指定返回值) ```java /** * 11 修改对未预设的调用返回默认期望(指定返回值) */ @Test public void test13() throws Exception { //mock对象使用Answer来对未预设的调用返回默认期望值 List mock = Mockito.mock(List.class, new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { return 999; } }); //下面的get(1)没有预设,通常情况下会返回NULL,但是使用了Answer改变了默认期望值 Assert.assertEquals(999, mock.get(1)); //下面的size()没有预设,通常情况下会返回0,但是使用了Answer改变了默认期望值 Assert.assertEquals(999, mock.size()); } ``` 12 用spy监控真实对象,设置真实对象行为 ```java /** * 12 用spy监控真实对象,设置真实对象行为 */ @Test(expected = IndexOutOfBoundsException.class) public void spy_on_real_objects(){ List list = new LinkedList(); List spy = Mockito.spy(list); //下面预设的spy.get(0)会报错,因为会调用真实对象的get(0),所以会抛出越界异常 //Mockito.when(spy.get(0)).thenReturn(3); //使用doReturn-when可以避免when-thenReturn调用真实对象api Mockito.doReturn(999).when(spy).get(999); //预设size()期望值 Mockito.when(spy.size()).thenReturn(100); //调用真实对象的api spy.add(1); spy.add(2); Assert.assertEquals(100,spy.size()); Assert.assertEquals(1,spy.get(0)); Assert.assertEquals(2,spy.get(1)); Assert.assertEquals(999,spy.get(999)); } ``` 13 不做任何返回 ```java /** * 13 不做任何返回 */ @Test public void test14() { A a = Mockito.mock(A.class); //void 方法才能调用doNothing() Mockito.doNothing().when(a).setName(Mockito.anyString()); a.setName("bb"); Assert.assertEquals("bb", a.getName()); } class A { private String name; private void setName(String name) { this.name = name; } private String getName() { return name; } } ``` 14 调用真实的方法 ```java /** * 14 调用真实的方法 */ @Test public void Test() { B a = Mockito.mock(B.class); //void 方法才能调用doNothing() Mockito.when(a.getName()).thenReturn("bb"); Assert.assertEquals("bb",a.getName()); //等价于Mockito.when(a.getName()).thenCallRealMethod(); Mockito.doCallRealMethod().when(a).getName(); Assert.assertEquals("zhangsan",a.getName()); } class B { public String getName(){ return "zhangsan"; } } ``` 15 重置mock ```java /** * 15 重置mock */ @Test public void reset_mock(){ List list = mock(List.class); Mockito. when(list.size()).thenReturn(10); list.add(1); Assert.assertEquals(10,list.size()); //重置mock,清除所有的互动和预设 Mockito.reset(list); Assert.assertEquals(0,list.size()); } ``` 16 @Mock注解 ```java /** * 16 @Mock 注解 */ public class MockitoTest { @Mock private List mockList; //必须在基类中添加初始化mock的代码,否则报错mock的对象为NULL public MockitoTest() { MockitoAnnotations.initMocks(this); } @Test public void AnnoTest() { mockList.add(1); Mockito.verify(mockList).add(1); } } ``` 17 指定测试类使用运行器:MockitoJUnitRunner ```java /** * 17 指定测试类使用运行器:MockitoJUnitRunner */ @RunWith(MockitoJUnitRunner.class) public class MockitStudy03 { @Mock private List mockList; @Test public void AnnoTest() { mockList.add(1); Mockito.verify(mockList).add(1); } } ``` 18 @MockBean 特点 **1 可以使用接口,也可以使用具体的接口实现类** **2 对代理对象的Bean兼容也可以,因为本质上@Mock|@MockBean 是一个虚拟对象,没有stub就返回默认值** **3 如果对象是代理对象,SpringBean 走真实调用还是无法解决成功,无论是否引入mock-line 2.7xxx** ```java @Controller public class DemoAction { public String getUserName(String id) { return id+"张三"; } } /** * 18 @MockBean */ @RunWith(SpringRunner.class) @SpringBootTest public class MockBean01 { /** * 项目启动时排除掉的bean * 替换为Mockit操作的Bean * 是集成在Spring容器中的 @Mock 用来替换Bean对象 * @MockBean 只能 mock 本地的代码——或者说是自己写的代码,对于储存在库中而且又是以 Bean 的形式装配到代码中的类无能为力。 */ @MockBean private DemoAction demoAction; @Test public void testGetUserName() throws Exception { Mockito.when(demoAction.getUserName("1")).thenReturn("李四"); System.out.println(demoAction.getUserName("1")); } https://www.jianshu.com/p/ecbd7b5a2021 ``` ` @MockBean` 只能 mock 本地的代码——或者说是自己写的代码,对于储存在库中而且又是以 Bean 的形式装配到代码中的类无能为力 `@SpyBean` 解决了 SpringBoot 的单元测试中 `@MockBean` 不能 mock 库中自动装配的 Bean 的局限 19 spy 与 mock的不同 **spy 和 mock不同,不同点是:** **spy 的参数是对象示例,mock 的参数是 class。 被 spy 的对象,调用其方法时默认会走真实方法。mock 对象不会。** **spy 如果没有打桩的方法会走真实的调用对象,如果打桩了会自走打桩的操作,本质上还会走一次** **mock 没有打桩就返回默认返回值,不走真实方法 打桩了就走打桩的操作** ```java /** * 描述: describe * 19 spy 与 mock的不同 * @author shengyang5 * @version 2021/7/22 15:03 */ class ExampleService { int add(int a, int b) { System.out.println("被调用"); return a + b; } } public class SpyStudy01 { /** * spy 和 mock不同,不同点是: *

* spy 的参数是对象示例,mock 的参数是 class。 * 被 spy 的对象,调用其方法时默认会走真实方法。mock 对象不会。 *

* spy 如果没有打桩的方法会走真实的调用对象,如果打桩了会自走打桩的操作,本质上还会走一次 *

* mock 没有打桩就返回默认返回值,不走真实方法 * 打桩了就走打桩的操作 */ @Test public void test() throws Exception { ExampleService spy = spy(new ExampleService()); // 没有打桩 默认走真实方法 int add = spy.add(1, 2); Assert.assertEquals(3, add); System.out.println("add = " + add); // 打桩后 不走了,但是实际上还是会先执行一次 用when去设置模拟返回值时,它里面的方法(spy.add())会先执行一次。 when(spy.add(Mockito.anyInt(), Mockito.anyInt())).thenReturn(10); add = spy.add(1, 2); System.out.println("add = " + add); Assert.assertEquals(10, add); } } ``` 20 @Spy 的缺陷 **1mockito 1.0版本中使用@Spy监视接口类,会报错,2.0版本中不会报错,但是不支持接口类、内部类、抽象类(这是个大坑)** **2对于@Spy,如果发现修饰的变量是 null,会自动调用类的无参构造函数来初始化** **3` @Spy` 如果是修饰接口的话,不能调用的到真实的方法,而且还不报错** **4如果要真实模拟,必须指定到接口的实现类,而且实现类的其他 资源类,你也需要手动的@mock或者@spy加入** **5不能自动的处理类里的Bean的依赖关系** ```java package com.codeyang.mocktest2.mockit; import org.junit.Assert; import org.junit.Test; import org.mockito.MockitoAnnotations; import org.mockito.Spy; import static org.mockito.Mockito.*; /** * 描述: * 20 @Spy 的缺陷 * * @author shengyang5 * @version 2021/7/22 15:26 */ class ExampleService2 { int add(int a, int b) { return a + b; } } public class SpyStudy02 { /** * mockito 1.0版本中使用@Spy监视接口类,会报错,2.0版本中不会报错,但是不支持接口类、内部类、抽象类(这是个大坑) * 对于@Spy,如果发现修饰的变量是 null,会自动调用类的无参构造函数来初始化 * * 注意: * @Spy 如果是修饰接口的话,不能调用的到真实的方法,而且还不报错 * 如果要真实模拟,必须指定到接口的实现类,而且实现类的其他 资源类,你也需要手动的@mock或者@spy加入 * 不能自动的处理类里的Bean的依赖关系 */ /** * 写法1 * * @Spy private ExampleService spyExampleService; * 写法2 * @Spy private ExampleService spyExampleService = new ExampleService(); * 注意: 如果没有无参的构造方法那么就要手动new对象 */ @Spy private ExampleService2 spyExampleService2; @Test public void test_spy() { MockitoAnnotations.initMocks(this); Assert.assertEquals(3, spyExampleService2.add(1, 2)); when(spyExampleService2.add(1, 2)).thenReturn(10); Assert.assertEquals(10, spyExampleService2.add(1, 2)); } /** * @Spy 可以修饰接口,抽象类等 但是调用方法不报错,返回默认值 */ @Spy private Isa isa; @Test public void isaTest() { MockitoAnnotations.initMocks(this); String name = isa.loadName(); System.out.println(name); } /** * @Spy 本身需要支持到接口具体实现类 */ @Spy private IsaImpl isaImpl; @Test public void isaImplTest() { MockitoAnnotations.initMocks(this); String name = isaImpl.loadName(); System.out.println(name); } } interface Isa { String loadName(); } class IsaImpl implements Isa { @Override public String loadName() { return "1111"; } } ``` 21 @SpyBean 与 Spring框架+Mockit集成使用 **1使用SpringRunner 就可以得到Spring的容器的上下文** **2SpringTest 是走SpringTest的一套流程** `@SpyBean` **1 继承了@Spy的特性 方法调用没有打桩走真实第哦啊有** **2 比@Spy更强可以兼容普通的Bean对象创建的对象 可以调用非代理对象的真实方法** **(测试对于JDK的动态代理对象兼容不行,CGLIB的没有测试)** **3 对于Mybatis等动态代理的对象 要支持必须要引入mockito-inline 2.7 以上** **4 @SpyBean 比 @Spy 更智能,可以自动主力类其他资源的依赖关系,并自动加入** **5 @Spy 只支持到具体的类 @SpyBean可以支持接口-可以把spring的bean对象自动 spy** ```java package com.codeyang.mocktest2.mockit; import com.codeyang.mocktest2.entity.TUser; import com.codeyang.mocktest2.mapper.TUserMapper; import com.codeyang.mocktest2.service.TUserService; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.MockitoAnnotations; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.context.junit4.SpringRunner; import java.util.List; /** * 描述: * 21 RunWith 是启动什么上下文 * 1使用SpringRunner 就可以得到Spring的容器的上下文 * 2SpringTest 是走SpringTest的一套流程 * * @author shengyang5 * @version 2021/7/22 15:29 */ @RunWith(SpringRunner.class) @SpringBootTest public class SpyBeanStudy01 { /** * 如果要使用@Mock @Spy 原生注解需要前置启动 */ @Before public void Before() throws Exception { MockitoAnnotations.initMocks(this); } /** * @SpyBean | * 1 继承了@Spy的特性 方法调用没有打桩走真实第哦啊有 * 2 比@Spy更强可以兼容普通的Bean对象创建的对象 可以调用非代理对象的真实方法 * (测试对于JDK的动态代理对象兼容不行,CGLIB的没有测试) * 3 对于Mybatis等动态代理的对象 要支持必须要引入mockito-inline 2.7 以上 * 4 @SpyBean 比 @Spy 更智能,可以自动主力类其他资源的依赖关系,并自动加入 * 5 @Spy 只支持到具体的类 @SpyBean可以支持接口-可以把spring的bean对象自动 spy */ // @Spy // 如果是@Spy 就会报错,因为其 它无法将 userService 里的 TUserMapper处理 // private TUserServiceImpl userService; @SpyBean private TUserService userService; @Test public void userServiceQueryAll() throws Exception { List tUsers = userService.queryAll(); System.out.println(tUsers); } /** * 对于动态代理对象无法直接mock 需要引入第三方jar mock-inline */ @SpyBean private TUserMapper userMapper; @Test public void userMapperQueryAll() throws Exception { List tUsers = userMapper.selectAll(); System.out.println(tUsers); } } ``` ==@InjectMocks== **模拟创建一个实例,简单的说是这个Mock可以调用真实代码的方法,其余用@Mock(或@Spy)注解创建的mock将被注入到用该实例中。** **1 InjectMocks 对象 代表这个对象可以被打桩** **2一般修饰被测试的类** **3InjectMocks 创建这个类的对象并自动将@Mock ,@Spy 等注解的属性值注入到这个中** **4要求必须是类不能是接口** ``` 1、Mockito部分: a. @InjectMocks:模拟创建一个实例,简单的说是这个Mock可以调用真实代码的方法,其余用@Mock(或@Spy)注解创建的mock将被注入到用该实例中。 b. @Mock、@Spy:两者均表示mock对象,但是用法有区别 ​ (1). @Mock表示对函数的调用均执行mock(即虚假函数),不执行真正部分,如果未对调用链mock,调用时将返回默认值(null、false、0) ​ (2). @Spy表示对函数的调用均执行真正部分,只有mock的调用链才会走mock,未mock打桩的调用链,走真实调用 ​ (3). mockito 1.0版本中使用@Spy监视接口类,会报错,2.0版本中不会报错,但是不支持接口类、内部类、抽象类(这是个大坑) c. when方法: ​ (1). when方法不会孤独使用,一般结合stub打桩同时使用 ​ (2). when方法是作为mock方法时的匹配条件 ​ (3). when方法可结合doCallRealMethod()、doReturn()、thenReturn()等方法使用 ​ (4). when()对调用方法mock时,可传入具体的参数值,也可使用匹配器(比如:anyString()、any()、anyInt()、isNull()、isNotNull()等) ​ (5). 如果调用方法传入的是dto、model之类的对象,那么在不修改开发代码的基础上,可以使用argThat()自定义参数匹配器(核心使用方法),重写匹配器方法 ​ (6). 如果接口请求是post,且入参是对象,传参是json串,那么序列化后,会导致参数传入controller层接口时,内存地址变了,而mockito默认的是使用equals方法匹配, ​ 此时内存地址已变,会导致when()mock条件匹配失败,此时需要使用argThat()自定义参数匹配器,具体实现见下面示例 d. stub打桩: ​ (1). 具体值打桩,常用方法有doReturn()、thenReturn(),两者使用场景不同,前者用于@Spy场景,后者用于@Mock场景,且两者在when的前后顺序不一样 ​ (2). 特殊打桩, doCallRealMethod() -→ 走实际真实调用链 ​ doNothing() -→ 什么事都不做,不进行任何打桩(常用语void调用方法) ​ doAnswer() -→ 和doReturn很相似 ​ doThrow() -→ 打桩抛出异常 2、MockMvc部分: a. 测试逻辑 ​ (1). MockMvcBuilder构造MockMvc的构造器 ​ (2). MockMvcRequestBuilders创建request请求(包括请求头部信息、入参、cookis) ​ (3). mockMvc调用perform,执行一个RequestBuilder请求,调用controller的业务处理逻辑 ​ (4). perform返回ResultActions,返回操作结果,可以通过ResultActions、MockMvcResultMatchers、MvcResoult、Junit等方式对结果进行打印、验证 b. 方法细解 ​ (1). perform:执行一个RequestBuilder请求,会自动执行SpringMvc的流程并映射到相应的控制器执行处理 ​ (2). andExpect:添加ResultMatcher验证规则,验证控制器执行完成后结果是否正确(对返回的数据进行的判断) ​ (3). andDo:添加ResultHandler结果处理器,比如调试时打印结果到控制台(对返回的数据进行的判断) ​ (4). andReturn:最后返回相应的MvcResult:然后进行自定义验证/进行下一步异常处理(对返回的数据进行的判断) ​ (5). param:添加request的参数,如发送请求的时候戴上了schoolId=10001的参数,可用于get请求,假如使用需要发送json数据格式的时候将不能使用这种方式(比如post请求) ​ (6). content:添加request参数,string格式,可用于post请求,传送json串参数 c. 两种不同构造MockMvc构造器的方法 ​ (1). 集成web环境方式 ​ MockMvcBuilders.webAppContextSetup(WebApplicationContext context):指定WebApplicationContext,将会从该上下文获取相应的控制器并得到相应的MockMvc ​ (2). 独立测试方式 ​ MockMvcBuilders.standaloneSetup(Object... controllers):通过参数指定一组控制器,这样就不需要从上下文获取了 ``` ```java /** * 描述: 22 @InjectMocks * * @author shengyang5 * @version 2021/7/23 13:49 */ @RunWith(SpringRunner.class) @SpringBootTest public class InjectMocksTest { @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); } /** * 如果@InjectMocks 是一个接口 需要 new 指定类 * 不能是接口 */ @InjectMocks // 不注入也可以 @Autowired private MvcController mvcController; @SpyBean private TUserService tUserService; @Test public void getAll() { List tUsers = new ArrayList<>(); tUsers.add(new TUser(999, "测试数据", 99, "古巴")); when(tUserService.queryAll()).thenReturn(tUsers); System.out.println(mvcController.getAll()); if (tUsers.equals(mvcController.getAll())) { System.out.println(true); } } } ``` ## MockMvc #### **背景** MockMvc 是SpringMvc 也就是SpringWeb解决方案中的一环,设计目的就是为了针对Web层的测试,是针对SpringMvc的测试不同于Junit针对Service层的测试 本质上是拿到了SpringMvc的WebContenxt 是天然集成在WebTest的jar包中 就是SpringMvc下面的一个类 MockMvc是由spring-test包提供,实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用,使得测试速度快、不依赖网络环境。同时提供了一套验证的工具,结果的验证十分方便。 #### 为什么使用 ​ 普通的原始测试 启动容器-->浏览器请求-- 最耗时操作 ​ PostMan模拟数据 启动容器---> postman构造请求 ​ 不需要我们单独引入,可以在不启动web容器的情况下也可以直接使用web层的处理器来测试,直接使用代码方式来测试 ​ Mockvc 不会收到网络环境的波动影响,不依赖网络,在分布式开发中这个测试就有很大的优点 #### 优点 ​ MockMvc是springMvc其下的类,pom文件中不需要过多的额外依赖 ​ MockMvc对http接口请求,无需启动本地服务,节省时间,且方便测试后的代码修改 ​ MockMvc提供了灵活的mvc环境模拟的方式,包括独立测试方式、集成web环境方式 ​ MockMvc支持get、post、delete、put、file文件上传等多种接口请求方式 ​ MockMvc支持session,且可以直接跨过鉴权,可以无需编写鉴权部分 ​ MockMvc实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用,这样可以使得测试速度快、不依赖网络环境, ​ 而且提供了一套验证的工具,这样可以使得请求的验证统一而且很方便 ​ 使用MockMvc模拟controller请求,无需启动本地服务 #### 用法 接口MockMvcBuilder,提供一个唯一的build方法,用来构造MockMvc。主要有两个实现:StandaloneMockMvcBuilder和DefaultMockMvcBuilder,分别对应两种测试方式,即独立安装和集成Web环境测试(并不会集成真正的web环境,而是通过相应的Mock API进行模拟测试,无须启动服务器)。MockMvcBuilders提供了对应的创建方法standaloneSetup方法和webAppContextSetup方法,在使用时直接调用即可。 #### 注意 **如果接口请求是post,且入参是对象,传参是json串,那么序列化后,会导致参数传入controller层接口时,内存地址变了,而mockito默认的是使用equals方法匹配,** **此时内存地址已变,会导致when()mock条件匹配失败,此时需要使用argThat()自定义参数匹配器,** #### a. 测试逻辑 (1). MockMvcBuilder构造MockMvc的构造器 (2). MockMvcRequestBuilders创建request请求(包括请求头部信息、入参、cookis) (3). mockMvc调用perform,执行一个RequestBuilder请求,调用controller的业务处理逻辑 (4). perform返回ResultActions,返回操作结果,可以通过ResultActions、MockMvcResultMatchers、MvcResoult、Junit等方式对结果进行打印、验证 #### b. 方法细解 (1). perform:执行一个RequestBuilder请求,会自动执行SpringMvc的流程并映射到相应的控制器执行处理 (2). andExpect:添加ResultMatcher验证规则,验证控制器执行完成后结果是否正确(对返回的数据进行的判断) (3). andDo:添加ResultHandler结果处理器,比如调试时打印结果到控制台(对返回的数据进行的判断) (4). andReturn:最后返回相应的MvcResult:然后进行自定义验证/进行下一步异常处理(对返回的数据进行的判断) (5). param:添加request的参数,如发送请求的时候戴上了schoolId=10001的参数,可用于get请求,假如使用需要发送json数据格式的时候将不能使用这种方式(比如post请求) (6). content:添加request参数,string格式,可用于post请求,传送json串参数 #### c. 两种不同构造MockMvc构造器的方法 (1). 集成web环境方式 MockMvcBuilders.webAppContextSetup(WebApplicationContext context):指定WebApplicationContext,将会从该上下文获取相应的控制器并得到相应的MockMvc (2). 独立测试方式 MockMvcBuilders.standaloneSetup(Object... controllers):通过参数指定一组控制器,这样就不需要从上下文获取了 #### MockMvc案列代码 ```java /** * 描述: describe * * @author shengyang5 * @version 2021/7/21 10:23 */ //SpringBoot1.4版本之前用的是SpringJUnit4ClassRunner.class @RunWith(SpringRunner.class) //SpringBoot1.4版本之前用的是@SpringApplicationConfiguration(classes = Application.class) @SpringBootTest //测试环境使用,用来表示测试环境使用的ApplicationContext将是WebApplicationContext类型的 @WebAppConfiguration public class MockMvcTest { /** * 注入 SpringWebContext */ @Autowired private WebApplicationContext webApplicationContext; /** * MockMvc对象 */ private MockMvc mockMvc; // 1 前提配置mockmvc @Before public void setup() { //两种实例化的方式 // 方式1 独立安装 // mockMvc = MockMvcBuilders.standaloneSetup(new MvcController()).build(); //方式2 集成web mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); } // 2 构建请求测试web /** * 最简单的请求构造 * * @throws Exception */ @Test public void test1() throws Exception { MockHttpServletRequestBuilder mockHttpServletRequestBuilder = MockMvcRequestBuilders.get("/api/get"); // 在执行mokmvc 之前将请求先放入 mockMvc.perform(mockHttpServletRequestBuilder); } /** * 1 * 请求 /api/getByid * 传参 9 * 打印 请求响应报文全部内容 * * @throws Exception */ @Test public void test2() throws Exception { RequestBuilder requestBuilder = MockMvcRequestBuilders .get("/api/getByid") .param("id", "9"); mockMvc.perform(requestBuilder) .andDo(MockMvcResultHandlers.print()); } /** * 2 * 请求 /api/getByid * 传参 9 * 拿到请求的对应响应内容 * * @throws Exception */ @Test public void testGet() throws Exception { /* * 1、mockMvc.perform执行一个请求。 * 2、MockMvcRequestBuilders.get("XXX")构造一个请求。 * 3、ResultActions.param添加请求传值 * 4、ResultActions.accept(MediaType.TEXT_HTML_VALUE))设置返回类型 * 5、ResultActions.andExpect添加执行完成后的断言。 * 6、ResultActions.andDo添加一个结果处理器,表示要对结果做点什么事情 * 比如此处使用MockMvcResultHandlers.print()输出整个响应结果信息。 * 7、ResultActions.andReturn表示执行完成后返回相应的结果。 */ RequestBuilder requestBuilder = MockMvcRequestBuilders .get("/api/getByid") .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) .param("id", "9") .accept(MediaType.APPLICATION_JSON_UTF8_VALUE); ResultActions resultActions = mockMvc.perform(requestBuilder) .andExpect(MockMvcResultMatchers.status().isOk()) .andDo(MockMvcResultHandlers.print()); System.out.println(resultActions.andReturn().getResponse().getContentAsString()); } /** * 3 * 简化写法使用静态导入 * * @throws Exception */ @Test public void get3() throws Exception { // 简化写法,可以导入静态方法 ResultActions resultActions = mockMvc.perform( get("/api/getByid") .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) .param("id", "9") .accept(MediaType.APPLICATION_JSON_UTF8) ) .andExpect(MockMvcResultMatchers.status().isOk()) .andDo(MockMvcResultHandlers.print()); System.out.println("返回结果=" + resultActions.andReturn().getResponse().getContentAsString()); } /** * 4 * 测试restfull风格 * * @throws Exception * @RequestParam 写法 */ @Test public void get4() throws Exception { ResultActions resultActions = mockMvc.perform( get("/api/getId") .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) .param("id", "9") .accept(MediaType.APPLICATION_JSON_UTF8) ) .andExpect(MockMvcResultMatchers.status().isOk()) .andDo(MockMvcResultHandlers.print()); System.out.println("返回结果=" + resultActions.andReturn().getResponse().getContentAsString()); } /** * 4 * 测试restfull风格 * 测试@PathVariable * * @throws Exception */ @Test public void get5() throws Exception { ResultActions resultActions = mockMvc.perform( get("/api/get/9") .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) .accept(MediaType.APPLICATION_JSON_UTF8) ) .andExpect(status().isOk()) .andDo(print()); System.out.println("返回结果=" + resultActions.andReturn().getResponse().getContentAsString()); } /** * post请求 * * @throws Exception */ @Test public void test6() throws Exception { // 最简化写法 System.out.println(mockMvc.perform( post("/api/getNameAndAdd") // 请求地址 .param("name", "小王") // 参数 param .param("address", "安徽") .accept(MediaType.APPLICATION_JSON_UTF8) // 期望响应什么类型 ).andExpect(status().isOk()) // expect 校验 .andDo(print()) // 请求结束后执行什么操作 .andReturn().getResponse().getContentAsString()); // 返回响应信息 } /** * json 请求 * put 方式 * * @throws Exception */ @Test public void test7() throws Exception { // 测试请求是json数据格式 ObjectMapper objectMapper = new ObjectMapper(); TUser tUser = objectMapper.readValue("{\"id\":7,\"name\":\"小王老头\",\"age\":20,\"address\":\"安徽\"}", TUser.class); tUser.setAddress("北京"); String jsonStr = objectMapper.writeValueAsString(tUser); MvcResult mvcResult = mockMvc.perform( put("/api/putObj") .contentType(MediaType.APPLICATION_JSON_UTF8) .content(jsonStr) .accept(MediaType.APPLICATION_JSON_UTF8) ) .andExpect(status().isOk()) .andDo(print()) .andReturn(); System.out.println(mvcResult.getResponse().getContentAsString()); } /** * 测试文件上传 * * @throws Exception */ @Test public void test() throws Exception { FileInputStream inputStream = new FileInputStream("D:\\CodeWorkSpace\\mocktest2\\src\\main\\resources\\file.txt"); byte[] bytes = toByteArray(inputStream); MockMultipartFile firstFile = new MockMultipartFile("uploadFile", "file.txt", "text/plain", bytes); ResultActions resultActions = mockMvc.perform(fileUpload("/api/upload") .file(firstFile)) .andExpect(status().isOk()) .andDo(print()); System.out.println("fileContent = " + resultActions.andReturn().getResponse().getContentAsString()); } /** * InputStream 转换成byte[] * * @param input * @return * @throws IOException */ private static byte[] toByteArray(InputStream input) throws IOException { ByteArrayOutputStream output = new ByteArrayOutputStream(); byte[] buffer = new byte[1024 * 4]; int n = 0; while (-1 != (n = input.read(buffer))) { output.write(buffer, 0, n); } return output.toByteArray(); } } ``` ## 最终极版本合集测试案列 ```java /** * 描述: describe * * @author shengyang5 * @version 2021/7/23 14:27 */ @RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest @WebAppConfiguration public class BaseUnitTest { protected MockMvc mockMvc; @Before public void setup() throws Exception { MockitoAnnotations.initMocks(this); } @Test public void emptyTest() { // 由于自动test必须有方法,所以加一个空的Test方法 } } /** * 描述: Mockito+MockMvc+Junit 自动集成测试 * SpringMVC单元测试样例代码 * * @author shengyang5 * @version 2021/7/23 14:26 */ public class MockitoMockMvcJunitTest extends BaseUnitTest { @InjectMocks private MvcController mvcController; /** * MvcController 需要 userService * 所以注入 * userService * 使用@SpyBean注入 走真实调用 */ @SpyBean private TUserService userService; @Override public void setup() throws Exception { super.setup(); mockMvc = MockMvcBuilders.standaloneSetup(mvcController).build(); } /** * post请求请求的相关操作 * * @throws Exception */ @Test public void testPost() throws Exception { //mock 操作 List list = new ArrayList<>(); list.add(new TUser(99, "还在开发中", 99, "湖南")); // 模拟这个 需要被调用的方法,这个接口的方法可能是RPC远程调用比较耗时,也可能还没有开发完成,我们直接模拟出来, when(userService.queryAddressAndName(Mockito.anyString(), Mockito.anyString())) .thenReturn(list); // 构建请求 MockHttpServletRequestBuilder post = post("/api/getNameAndAdd") // 请求地址 .param("name", "小王") // 参数 param .param("address", "安徽") // 参数 param .contentType(MediaType.APPLICATION_JSON_UTF8) .accept(MediaType.APPLICATION_JSON_UTF8);// 期望响应什么类型 // 执行请求 ResultActions resp = mockMvc.perform(post); // 对结添加预期 String result = resp // 期望放回状态 .andExpect(status().isOk()) // 打印请求报文 .andDo(print()) // 得到响应内容 .andReturn().getResponse().getContentAsString(); System.out.println("result = " + result); } /** * 测试put * * @throws Exception */ @Test public void testPut() throws Exception { // 1 构造模拟请求测试需要的输入数据 ObjectMapper objectMapper = new ObjectMapper(); TUser tUser = new TUser(100, "还在开发中put", 20, "shangHai"); String jsonStr = objectMapper.writeValueAsString(tUser); // 2 构建中间步骤 的返回数据 // 这里涉及到了JSON数据的序列化,需要手动写请求参数匹配 when(userService.updateUser(Mockito.argThat(new IsaMatcher()))).thenReturn(1); MvcResult mvcResult = mockMvc.perform( put("/api/putObj") .contentType(MediaType.APPLICATION_JSON_UTF8) .content(jsonStr) .accept(MediaType.APPLICATION_JSON_UTF8) ) .andExpect(status().isOk()) .andDo(print()) .andReturn(); System.out.println(mvcResult.getResponse().getContentAsString()); } class IsaMatcher implements ArgumentMatcher { @Override public boolean matches(TUser argument) { //这里就可以规定好对象的比对操作 // 无脑返回true return true; } } } ``` 项目案列的demo 参考资料如下: 1. https://www.jianshu.com/p/ecbd7b5a2021 2. https://blog.csdn.net/qq_28988969/article/details/105496849 3. https://docs.spring.io/spring-framework/docs/4.1.8.RELEASE_to_4.1.9.RELEASE/Spring%20Framework%204.1.9.RELEASE/org/springframework/test/web/servlet/MockMvc.html 4. https://blog.csdn.net/wo541075754/article/details/88983708