Mockito以及如何烹饪

关于文章


这是Mockito的另一本指南。 在其中,一方面,我试图描述该库的功能,以便使不熟悉该库的读者立即有机会充分利用它,而不仅仅是对其的一般了解。 另一方面,我想使它足够紧凑和结构化,这样我就可以快速完整地阅读它,并在阅读后很快找到但忘记了的东西。 总的来说,这篇文章对我本人很有用,当我刚接触该库而并不真正了解它的工作原理时。


我想它现在对我有用-有时我会忘记其中的一些,并且最方便的是根据我自己的需求(例如摘要)而不是根据官方文档或其他人的文章来回顾这些资料。 同时,我尝试构建文本,以便首先从零开始了解Mockito时非常方便,并且在某些地方,我详细分析了看似明显的事情-并非所有这些东西从一开始就对我来说是显而易见的。


内容:


  1. Mockito:这是什么,为什么有必要
  2. 环境,版本和实验动物
  3. 模拟和间谍
  4. 行为管理
    1. 设定通话条件
    2. 设定通话结果
  5. 跟踪方法调用
  6. 将对象模拟为字段值和Mockito批注
  7. 回滚行为到默认会话和Mockito会话
  8. 还有什么

Mockito:这是什么,为什么有必要


简而言之,Mockito是一个存根框架。


如您所知,在测试代码(主要是单元测试,但不仅是单元测试)时,被测元素通常需要提供工作时应使用的类的实例。 但是,它们通常不必完全发挥功能-相反,要求它们以严格定义的方式进行操作,以便其操作简单且完全可预测。 它们称为存根。 要获得它们,您可以创建接口的替代测试实现,使用功能的重新定义继承必要的类,依此类推,但这都是非常不便,多余且充满错误的。 在所有意义上,更方便的解决方案是用于创建存根的专用框架。 其中之一(也许是Java中最著名的)是Mockito。


Mockito允许您用一行代码创建任何类的所谓的模拟(类似于所需存根的基础)。 对于这样的模拟,创建后立即具有某种默认行为(所有方法都返回先前已知的值-通常为null0 )。 您可以按照自己的方式重新定义此行为,以适当的详细程度对其进行控制,等等。 结果,模拟成为具有所需属性的存根。 下面,我将详细讨论如何执行此操作。


我注意到,还可以为这些类创建模拟,实际上,您不能仅仅创建它们的新实例,尤其是具有专有私有构造函数的类,例如单例和实用程序类,并且框架和枚举的最小配置。


环境,版本和实验动物


在撰写本文时,我使用了:


  • Mockito:“ org.mockito:mockito-core:2.24.0”(撰写本文时为最新稳定版本)
  • TestNG:“ org.testng:testng:6.14.3”作为测试框架
  • AssertJ:“ org.assertj:assertj-core:3.11.1”作为验证工具
  • 龙目岛:'org.projectlombok:龙目岛:1.18.6'(为方便起见)
  • Java 8

对于我的非人为实验,我编写了该服务的接口,该接口提供对某些数据的访问。


 public interface DataService { void saveData(List<String> dataToSave); String getDataById(String id); String getDataById(String id, Supplier<String> calculateIfAbsent); List<String> getData(); List<String> getDataListByIds(List<String> idList); List<String> getDataByRequest(DataSearchRequest request); } 

并将请求类的此代码(为了方便起见)传递给最后一个接口方法。


 @AllArgsConstructor @Getter class DataSearchRequest { String id; Date updatedBefore; int length; } 

数据单元由ID标识,并具有更多特征,但是直接由服务返回的形式是字符串,而不是更复杂的对象。 我不会错过任何重要的事情,这些示例更加简单明了。


我马上会注意到:在下面的示例中,为了清楚起见,我直接调用了模拟对象的重写方法,但是经过实际测试,这个想法根本没有! 在此测试中,我将始终执行以下操作:


  • 根据需要配置我的服务的模拟;
  • 将它(很可能通过构造函数)传递给使用它的另一个类的实例(假设它使用DataService提供的数据包含某种业务逻辑),我将对其进行实际测试;
  • 启用测试类的功能并控制结果;
  • 如有必要,我将控制对我的模拟方法的调用的次数和顺序,由于先前的操作,被测试的类应该已经调用了该方法。

模拟和间谍


实际上,应该通过它访问大多数功能的Mockito的中心类是一个称为Mockito的类(还有BDDMockito类,它以更适合BDD的形式提供了相同的可能性,但在此我不再赘述) 。 对功能的访问是通过其静态方法实现的。


要创建DataService类的模拟,我只需执行以下操作:


 DataService dataServiceMock = Mockito.mock(DataService.class); 

完成-我得到了所需班级的一个实例。 任何需要此类型参数的方法或构造函数(例如,我要测试的类的构造函数)都将接受它。 即使上瘾检查稍后等待它也将通过它:不仅instanceof DataService将返回true ,而且dataServiceMock.getClass() -即DataService.class 。 从某种形式上讲,以编程方式将模拟对象与普通对象区分开来是一项相当困难的任务,这是合乎逻辑的:毕竟,第一个对象的目的只是与第二个对象没有区别。 但是,Mockito具有用于此目的的工具Mockito.mockingDetails方法。 通过将其传递给任意对象,我得到了MockingDetails类的对象。 从Mockito的角度来看,它包含有关此对象表示什么的信息:它是模拟的,间谍的(请参见下文),如何使用,如何创建等等。


特别需要注意的是,当我尝试为final类或enum的模拟实例创建模拟或覆盖final方法的行为时。 在这种情况下,使用Mockito的默认行为,上面的代码正是由于这种情况而拒绝工作。 但是,这可以更改-只需在项目(使用项目目录树的标准设备)中创建文件test/resources/mockito-extensions/org.mockito.plugins.MockMaker然后在其中输入以下行:


 mock-maker-inline 

之后,您可以按照通常的方式模仿final类和枚举,以及覆盖final方法。


我使用的模拟程序尽可能没有特征:没有一个方法会对任何事物产生任何影响,并且对象类型的返回值将为null ,原始类型的返回值将为0 。 请注意:如果该方法返回一个集合,则默认的模拟将不会返回null ,而将返回空的集合实例。 例如,对于List无论实际方法应该返回什么List这都将是一个空的LinkedList 。 但是,作为数组,基元或对象的值,我得到null 。 可以使用MockSettings类的功能来更改默认行为(不仅是默认行为),但这很少需要。


一种或另一种方式,在大多数情况下,我不需要默认行为,在下一节中,我将详细分析如何设置所需的内容。


但是,如果我想将具有可用功能的真实类对象用作存根,仅重新定义其部分方法的操作,该怎么办? 如果我们谈论的是单元测试,那么这种需求通常(但并非总是如此)表明该项目在设计上并不可行,并且原则上不建议这样做。 但是,在某些情况下由于某些原因无法避免这种情况。 对于这种情况,Mockito具有所谓的间谍“间谍”。 与模拟不同,它们可以基于类和完成的对象来创建:


 DataService dataServiceSpy = Mockito.spy(DataService.class); // or DataService dataService = new DataService(); dataServiceSpy = Mockito.spy(dataService); 

在基于类创建间谍时,如果其类型是接口,则将创建常规的模拟对象,如果类型是类,则Mockito将尝试使用默认构造函数(不带参数)创建实例。 并且只有在没有这样的构造函数的情况下,才会发生错误并且测试将无法进行。


默认情况下,间谍对象的行为与常规类实例的行为相同,但是它们为我提供了与模拟对象相同的可能性:它们允许我重新定义其行为并监视其使用(请参阅以下部分)。 重要的一点是:间谍不是创建它的实例的包装器! 因此,调用spy方法不会影响原始实例的状态。


行为管理


因此,关于如何嘲笑或间谍来做我需要的事情。 此外,除非另有明确说明,否则我到处总是简单地写“ mock”,这意味着“ mock or spy”。


通常,控制模拟对象的行为归结为一个显而易见的概念:当模拟以这种方式(即以这样的参数调用这样的方法)受到影响时,它应该以这样的方式响应。 这个概念在Mockito类中有两种实现-开发人员建议在可能的情况下使用的主要实现,而在主要不合适的情况下使用另一种实现。


主要实现基于Mockito.when方法。 该方法将对模拟对象的重新定义方法的调用作为“参数”(通过这种方式固定检测到的动作),并返回OngoingStubbing类型的对象,该对象允许调用Mockito.then... family的方法之一(这是对此效果的反应方式)。 总之,在最简单的情况下,它看起来像这样:


 List<String> data = new ArrayList<>(); data.add("dataItem"); Mockito.when(dataService.getAllData()).thenReturn(data); 

完成此操作后,通过在getAllData()对象上调用getAllData()方法,可以得到清单第一行中指定的对象。


在这里,熟悉的“面向对象”直觉可能会带来某种故障,因此值得详细介绍。 从Java语法的角度来看,作为参数传递给when方法的值当然是重写方法返回的值。 对于模拟,这是一个空值;对于间谍,这是实际对象的方法返回的值。 但是,由于Mockito在幕后发挥了神奇的作用,仅when之后的模拟对象方法调用位于方括号内whenwhen方法才能正常工作(并且在启动时出错时不会崩溃)。


在定义Mockito中的模拟行为时,通常会使用类似的意识形态:通过调用(模拟对象或Mockito类的)方法,我试图不获取其返回的值,但以某种方式影响所使用的模拟对象方法的调用:它的边界,设定结果,观察其挑战等。 我承认,这听起来有些模糊,在第一次碰撞时看起来很奇怪,但是一旦弄清楚了,很快就开始觉得这种方法在使用存根的情况下是完全自然的。


将条件和调用结果链接在一起的另一种实现方法是Mockito.do...系列的方法。 这些方法允许您从调用结果开始设置行为,并返回Stubber类的对象,您可以使用该对象设置条件。 与上述相同的绑定方式如下所示:


 List<String> data = new ArrayList<>(); data.add("dataItem"); Mockito.doReturn(data).when(dataService).getData() 

有什么区别,为什么最好通过Mockito.when绑定,以及何时仍必须使用Mockito.do...的方法? 请注意:在第一个实现中,设置方法的行为时(在本例中为getAllData() ),将首先执行对尚未重新定义的版本的调用,然后才在Mockito的肠道中进行覆盖。 在第二种方法中,不会发生这样的调用- Stubber.when方法直接传递到Stubber.when方法,并使用可重写方法调用此方法返回的相同类型但性质不同的对象。 这种差异决定了一切。 通过Mockito.do...绑定在编译阶段根本无法控制我将调用的可重定义方法,以及它在类型上是否与给定的返回值兼容。 因此,通常Mockito.when更可取的-这样做没有错误。 但是在某些情况下,我想避免调用重写的方法-对于新创建的模拟,这样的调用是完全可以接受的,但是如果我已经重新定义了此方法或处理间谍,则可能是不可取的,并且抛出异常将根本不允许进行必要的重新定义。 。 在这里,通过Mockito.do...进行绑定即可Mockito.do...


没有Mockito.do...方法不能做的另一种情况是覆盖返回void的方法:挂起的Mockito.when参数不能与这种方法一起使用。 Mockito.doReturn在这里Mockito.doReturn ,但是有Mockito.doThrowMockito.doAnswer和很少Mockito.doNothing


接下来,我将更详细地考虑如何设置呼叫的条件和结果。 我只会考虑通过Mockito.when绑定-另一种方法在处理方面几乎完全相似。


设定通话条件


上面的示例涉及一种没有参数的方法,并且关联的调用条件可能是一件事-调用的事实。 一旦出现参数,情况就变得更加复杂。 至少,要调用要设置其行为的方法,我需要向其传递一些信息。 但是另一件事更重要:事实证明,我并不总是希望得到给定的反应,而仅当我使用满足某些要求的参数来调用它时。 DataService具有以下方法:


 String getDataItemById(String id) { // some code... } 

如果无论参数如何,都需要对此方法的任何调用设置响应,则必须使用Mockito.any方法:


 Mockito.when(dataService.getDataItemById(any())) .thenReturn("dataItem"); 

如果我需要模拟仅对参数的某个值作出反应,则可以直接使用此值,也可以使用Mockito.eq (如果等效)或Mockito.same (如果需要链接比较)的方法:


 Mockito.when(dataService.getDataItemById("idValue")) .thenReturn("dataItem"); // or Mockito.when(dataService.getDataItemById(Mockito.eq("idValue"))) .thenReturn("dataItem"); 

如果我希望参数满足某些要求,则可以使用多个方便的,相同Mockito类的专用静态方法(例如,可以在某些字符序列的开头或结尾处检查字符串的内容,模式匹配等)。 还有一个通用方法Mockito.argThat(及其原始类型的类似物)接受ArgumentMatcher功能接口的实现:


 Mockito.when(dataService.getDataById( Mockito.argThat(arg -> arg == null || arg.length() > 5))) .thenReturn("dataItem"); 

ArgumentMatchersAdditionalMatchers类使您可以使用此接口的一些有用的现成实现。 例如, AdditionalMatchers.orAdditionalMatchers.and允许您组合其他匹配器(注意:这些类的静态方法不会返回匹配器的实例,而只能访问它们!)


对于相同的方法,您可以针对参数的不同要求多次设置行为,并且以此方式定义的所有行为模型将同时起作用。 当然,在某些情况下,它们可能相交-例如,我将要求在参数的int值小于5时返回一个结果,而在接收到偶数时返回另一个结果。 在这种情况下,以后指定的行为优先。 因此,在定义复杂的行为模式时,您应该从最弱的要求开始(在极限内any() ),然后再转向更具体的要求。


当使用具有多个自变量的方法时,将根据逻辑与将指定的要求组合在一起,也就是说,要获得指定的结果,每个自变量都必须满足指定的要求。 尽管可能存在,但我没有找到设置任意组合方式的方法。


另外,在指定这种方法的行为时,不能Mockito和值的直接传递来组合静态Mockito方法。 使用Mockito.eqMockito.same


设定通话结果


调用模拟对象方法后,对象必须响应该调用。 主要的可能结果是返回结果并引发异常,而Mockito工具箱正是在这些选项上设计的。


在上面已经显示的最简单的情况下,对调用的响应是返回一个值。 我将再次给出他的代码:


 List<String> data = new ArrayList<>(); data.add("dataItem"); Mockito.when(dataService.getAllData()).thenReturn(data); 

请注意:您只能返回一个对象;没有用于原语的单独方法。 因此,如果该方法返回原始值,则在这种情况下将取消装箱。 在大多数情况下,这不会造成干扰,但是如果编译器认为不是,则您将不得不以某种方式同意他……或忍受他的警告。


抛出异常不再困难:


 Mockito.when(dataService.getDataById("invalidId")) .thenThrow(new IllegalArgumentException()); 

还有另一种方法:您可以创建一个异常对象并将其直接抛出,或者可以为Mockito仅提供一个异常类,以便自动创建它:


 Mockito.when(dataService.getDataById("invalidId")) .thenThrow(IllegalArgumentException.class); 

在这两种情况下,语法都允许您使用和检查异常,但是,如果异常类型与我要强制抛出此异常的方法不匹配,则Mockito将不允许您运行此类测试。


当使用类作为参数时,构造函数(甚至没有参数)以及直接字段初始化都将被忽略-绕过它们创建的对象(毕竟,这是Mockito!),因此抛出异常的所有字段都将为null 。 因此,如果异常的内容对您很重要(例如,某个具有默认值的type字段),则您将不得不放弃此方法并手动创建异常。


如果响应给定条件的调用时,您始终需要返回某个特定的,始终相同的结果值或始终引发相同的异常,并且在大多数情况下这些功能已足够,则这些响应选项非常适合。 但是,如果需要更大的灵活性怎么办? 假设我的方法接受值的集合,并返回与第一个值相关的另一个值的集合(例如,通过其ID集合获得数据对象的集合),并且我想在测试中对不同的输入集重复使用此模拟对象数据,每次获取对应的结果。 当然,您可以分别描述对每个特定参数集的反应,但是有一个更方便的解决方案Mockito.thenAnswer方法,又名Mockito.then 。 它接受Answer功能接口的实现,该接口的唯一方法是接收InvocationOnMock类的对象。 从后者中,我可以请求方法调用的参数(以数字的形式或以数组的形式一次或全部),并根据需要使用它们。 例如,您可以为我的集合中的每个元素获取一个与之对应的值,从它们中形成一个新的集合并返回它(注意:只是返回了期望的结果,而不是您期望的那样将其写入参数对象的某个字段):


 Mockito.when(dataService.getDataByIds(Mockito.any())) .thenAnswer(invocation -> invocation .<List<String>>getArgument(0).stream() .map(id -> { switch (id) { case "a": return "dataItemA"; case "b": return "dataItemB"; default: return null; } }) .collect(Collectors.toList())); 

从意识形态上讲,这类似于编写一个真实方法的模型:获取参数,处理,返回结果。 - , - , , , mock- .


Answer , , — , AnswersWithDelay , ReturnsElementsOf . .


: InvocationOnMockObject[] , generic-.


thenCallRealMethod . . mock-, spy-. mock , , - null . spy thenCallRealMethod spy ; , - .


thenAnswer : InvocationOnMock callRealMethod() — , "" - .


OngoingStubbing OngoingStubbing , , , . , . thenReturn thenThrow , varargs. .


 Mockito.when(dataService.getDataById("a")) .thenReturn("valueA1", "valueA2") .thenThrow(IllegalArgumentException.class); 

"valueA1 , — "valueA2 ( ), ( ) IllegalArgumentException .



: (mock' ), . , : , , . verify .


, , :


 Mockito.verify(dataService).getDataById(Mockito.any()); 

, getDataById , , . , Mockito, when , , , mock-. , , , when , — mock', (. ).


:


 Mockito.verify(dataService, Mockito.times(1)) .getDataById(Mockito.any()); 

Mockito.times ; Mockito.never . Mockito.atLeast ( Mockito.atLeastOnce 1) Mockito.atMost , , Mockito.only , , mock- (. . ).


, Mockito , VerificationAfterDelay VerificationWithTimeout , Mockito.after Mockito.timeout . 例如:


 Mockito.verify(dataService, Mockito.after(1000).times(1)) .getDataById(Mockito.any()); 

, mock , , , . . after timeout , , , — , . , timeout — . VerificationWithTimeout never atMost : .


, Mockito.any() . , , — Mockito , , . Mock- , , , , :


 dataService.getDataById("a"); dataService.getDataById("b"); Mockito.verify(dataService, Mockito.times(2)).getDataById(Mockito.any()); Mockito.verify(dataService, Mockito.times(1)).getDataById("a"); Mockito.verify(dataService, Mockito.never()).getDataById("c"); dataService.getDataById("c"); Mockito.verify(dataService, Mockito.times(1)).getDataById("c"); Mockito.verifyNoMoreInteractions(dataService); 

verifyNoMoreInteractions ( verifyZeroInteractions ) — - ( verify ) mock- — . : varargs, , , !


, , , . , InOrder :


 InOrder inOrder = Mockito.inOrder(dataService); 

varargs; — mock- , InOrder . verify , Mockito.verify :


 inOrder.verify(dataService, times(2)).saveData(any()); inOrder.verify(dataService).getData(); 

, saveData , getData . , InOrder , — .


, , — , . - , , — , , . ArgumentCaptor capture() . 例如:


 DataSearchRequest request = new DataSearchRequest("idValue", new Date(System.currentTimeMillis()), 50); dataService.getDataByRequest(request); ArgumentCaptor<DataSearchRequest> requestCaptor = ArgumentCaptor.forClass(DataSearchRequest.class); Mockito.verify(dataService, times(1)).getDataByRequest(requestCaptor.capture()); assertThat(requestCaptor.getAllValues()).hasSize(1); DataSearchRequest capturedArgument = requestCaptor.getValue(); assertThat(capturedArgument.getId()).isNotNull(); assertThat(capturedArgument.getId()).isEqualTo("idValue"); assertThat(capturedArgument.getUpdatedBefore()).isAfterYear(1970); assertThat(capturedArgument.getLength()).isBetween(0, 100); 

ArgumentCaptor , , ArgumentCaptor . getValue() , getAllValues() — . , , .


Mock- Mockito


, mock- , — @Mock - :


 MockitoAnnotations.initMocks(this); 

( , mock', )


spy @Spy@Mock … spy , , ? , — spy .


@Captor ArgumentCaptor — , , .


@InjectMocks . - Mockito, . mock- , . , . - , null , - . ( ) dependency injection.


Mockito


, : mock (spy, argument captor...), , , . , mock' — , . JUnit , , TestNG — . , , mock' , , , . . , , — , .


, mock- . TestNG @BeforeMethod ( @AfterMethod ). mock' , , ( JUnit — @Before ).


, , — Mockito.reset Mockito.clearInvocations . varargs, mock'. , . : (, ) , / mock' , — . , mock' . . , , .


(, ) — MockitoAnnotations.initMocks(this); 。 "" , Mockito.


— Mockito. . mock- , ( mock' ). , MockitoSession , . TestNG:


 @Mock DataService dataService; MockitoSession session; @BeforeMethod public void beforeMethod() { session = Mockito.mockitoSession() .initMocks(this) .startMocking(); } @Test public void testMethod() { // some code using the dataService field } @AfterMethod public void afterMethod() { session.finishMocking(); } 

, — , "" (, ) , .


?


Mockito: mock spy-, . , . , , :


  • Mockito mock- MockSettings ( — , mock' - );
  • mock-, MockingDetails ;
  • BDDMockito Mockito ;
  • ( JUnit Mockito, ).

Mockito . javadoc' Mockito .


, , .

Source: https://habr.com/ru/post/zh-CN444982/


All Articles