关于文章
这是Mockito的另一本指南。 在其中,一方面,我试图描述该库的功能,以便使不熟悉该库的读者立即有机会充分利用它,而不仅仅是对其的一般了解。 另一方面,我想使它足够紧凑和结构化,这样我就可以快速完整地阅读它,并在阅读后很快找到但忘记了的东西。 总的来说,这篇文章对我本人很有用,当我刚接触该库而并不真正了解它的工作原理时。
我想它现在对我有用-有时我会忘记其中的一些,并且最方便的是根据我自己的需求(例如摘要)而不是根据官方文档或其他人的文章来回顾这些资料。 同时,我尝试构建文本,以便首先从零开始了解Mockito时非常方便,并且在某些地方,我详细分析了看似明显的事情-并非所有这些东西从一开始就对我来说是显而易见的。
内容:
- Mockito:这是什么,为什么有必要
- 环境,版本和实验动物
- 模拟和间谍
- 行为管理
- 设定通话条件
- 设定通话结果
- 跟踪方法调用
- 将对象模拟为字段值和Mockito批注
- 回滚行为到默认会话和Mockito会话
- 还有什么
Mockito:这是什么,为什么有必要
简而言之,Mockito是一个存根框架。
如您所知,在测试代码(主要是单元测试,但不仅是单元测试)时,被测元素通常需要提供工作时应使用的类的实例。 但是,它们通常不必完全发挥功能-相反,要求它们以严格定义的方式进行操作,以便其操作简单且完全可预测。 它们称为存根。 要获得它们,您可以创建接口的替代测试实现,使用功能的重新定义继承必要的类,依此类推,但这都是非常不便,多余且充满错误的。 在所有意义上,更方便的解决方案是用于创建存根的专用框架。 其中之一(也许是Java中最著名的)是Mockito。
Mockito允许您用一行代码创建任何类的所谓的模拟(类似于所需存根的基础)。 对于这样的模拟,创建后立即具有某种默认行为(所有方法都返回先前已知的值-通常为null
或0
)。 您可以按照自己的方式重新定义此行为,以适当的详细程度对其进行控制,等等。 结果,模拟成为具有所需属性的存根。 下面,我将详细讨论如何执行此操作。
我注意到,还可以为这些类创建模拟,实际上,您不能仅仅创建它们的新实例,尤其是具有专有私有构造函数的类,例如单例和实用程序类,并且框架和枚举的最小配置。
环境,版本和实验动物
在撰写本文时,我使用了:
- 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);
在基于类创建间谍时,如果其类型是接口,则将创建常规的模拟对象,如果类型是类,则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
之后的模拟对象方法调用位于方括号内when
, when
方法才能正常工作(并且在启动时出错时不会崩溃)。
在定义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.doThrow
, Mockito.doAnswer
和很少Mockito.doNothing
。
接下来,我将更详细地考虑如何设置呼叫的条件和结果。 我只会考虑通过Mockito.when
绑定-另一种方法在处理方面几乎完全相似。
设定通话条件
上面的示例涉及一种没有参数的方法,并且关联的调用条件可能是一件事-调用的事实。 一旦出现参数,情况就变得更加复杂。 至少,要调用要设置其行为的方法,我需要向其传递一些信息。 但是另一件事更重要:事实证明,我并不总是希望得到给定的反应,而仅当我使用满足某些要求的参数来调用它时。 DataService
具有以下方法:
String getDataItemById(String id) {
如果无论参数如何,都需要对此方法的任何调用设置响应,则必须使用Mockito.any
方法:
Mockito.when(dataService.getDataItemById(any())) .thenReturn("dataItem");
如果我需要模拟仅对参数的某个值作出反应,则可以直接使用此值,也可以使用Mockito.eq
(如果等效)或Mockito.same
(如果需要链接比较)的方法:
Mockito.when(dataService.getDataItemById("idValue")) .thenReturn("dataItem");
如果我希望参数满足某些要求,则可以使用多个方便的,相同Mockito
类的专用静态方法(例如,可以在某些字符序列的开头或结尾处检查字符串的内容,模式匹配等)。 还有一个通用方法Mockito.argThat(及其原始类型的类似物)接受ArgumentMatcher功能接口的实现:
Mockito.when(dataService.getDataById( Mockito.argThat(arg -> arg == null || arg.length() > 5))) .thenReturn("dataItem");
ArgumentMatchers
和AdditionalMatchers
类使您可以使用此接口的一些有用的现成实现。 例如, AdditionalMatchers.or
和AdditionalMatchers.and
允许您组合其他匹配器(注意:这些类的静态方法不会返回匹配器的实例,而只能访问它们!)
对于相同的方法,您可以针对参数的不同要求多次设置行为,并且以此方式定义的所有行为模型将同时起作用。 当然,在某些情况下,它们可能相交-例如,我将要求在参数的int
值小于5时返回一个结果,而在接收到偶数时返回另一个结果。 在这种情况下,以后指定的行为优先。 因此,在定义复杂的行为模式时,您应该从最弱的要求开始(在极限内any()
),然后再转向更具体的要求。
当使用具有多个自变量的方法时,将根据逻辑与将指定的要求组合在一起,也就是说,要获得指定的结果,每个自变量都必须满足指定的要求。 尽管可能存在,但我没有找到设置任意组合方式的方法。
另外,在指定这种方法的行为时,不能Mockito
和值的直接传递来组合静态Mockito
方法。 使用Mockito.eq
或Mockito.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
. .
: InvocationOnMock
— Object[]
, 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() {
, — , "" (, ) , .
?
Mockito: mock spy-, . , . , , :
- Mockito mock-
MockSettings
( — , mock' - ); - mock-,
MockingDetails
; BDDMockito
Mockito
;- ( JUnit Mockito, ).
Mockito . javadoc' Mockito
.
, , .