日常 Mockito 单元测试从业者指南

已发表: 2022-03-11

在敏捷时代,单元测试已经成为强制性的,并且有许多工具可以帮助进行自动化测试。 一个这样的工具是 Mockito,它是一个开源框架,可让您创建和配置模拟对象以进行测试。

在本文中,我们将介绍如何创建和配置模拟,并使用它们来验证被测系统的预期行为。 我们还将深入了解 Mockito 的内部结构,以更好地了解其设计和注意事项。 我们将使用 JUnit 作为单元测试框架,但由于 Mockito 未绑定到 JUnit,即使您使用不同的框架,您也可以跟随。

获得 Mockito

如今,获得 Mockito 很容易。 如果您使用的是 Gradle,只需将这一行添加到您的构建脚本中:

 testCompile "org.mockito:mockito−core:2.7.7"

至于像我这样仍然喜欢 Maven 的人,只需将 Mockito 添加到您的依赖项中,如下所示:

 <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>2.7.7</version> <scope>test</scope> </dependency>

当然,世界比 Maven 和 Gradle 要广阔得多。 您可以自由使用任何项目管理工具从 Maven 中央存储库获取 Mockito jar 工件。

接近 Mockito

单元测试旨在测试特定类或方法的行为,而不依赖于它们的依赖项的行为。 由于我们正在测试最小的“单元”代码,因此我们不需要使用这些依赖项的实际实现。 此外,在测试不同的行为时,我们将使用这些依赖项的稍微不同的实现。 一种传统的、众所周知的方法是创建“存根”——适用于给定场景的接口的特定实现。 这样的实现通常具有硬编码的逻辑。 存根是一种测试替身。 其他种类包括假货、模拟物、间谍、假人等。

我们将只关注两种类型的测试替身,“模拟”和“间谍”,因为 Mockito 大量使用它们。

模拟

什么是嘲讽? 显然,这不是你取笑你的开发者伙伴的地方。 单元测试的模拟是当您创建一个以受控方式实现真实子系统行为的对象时。 简而言之,模拟被用作依赖项的替代品。

使用 Mockito,您可以创建一个模拟,告诉 Mockito 在调用特定方法时要做什么,然后在您的测试中使用模拟实例而不是真实的实例。 测试结束后,可以查询mock,查看调用了哪些具体方法,或者以状态改变的形式查看副作用。

默认情况下,Mockito 为 mock 的每个方法提供了一个实现。

间谍

间谍是 Mockito 创建的另一种类型的测试替身。 与模拟相反,创建间谍需要一个实例来监视。 默认情况下,间谍将所有方法调用委托给真实对象,并记录调用了哪些方法以及使用了哪些参数。 这就是使它成为间谍的原因:它正在监视一个真实的对象。

尽可能考虑使用模拟而不是间谍。 间谍可能对测试无法重新设计为易于测试的遗留代码很有用,但需要使用间谍来部分模拟一个类表明一个类做得太多,从而违反了单一责任原则。

构建一个简单的例子

让我们看一个可以编写测试的简单演示。 假设我们有一个UserRepository接口,它有一个通过标识符查找用户的方法。 我们还有密码编码器的概念,可以将明文密码转换为密码哈希。 UserRepositoryPasswordEncoder都是通过构造函数注入的UserService的依赖项(也称为协作者)。 这是我们的演示代码的样子:

用户存储库
public interface UserRepository { User findById(String id); }
用户
public class User { private String id; private String passwordHash; private boolean enabled; public User(String id, String passwordHash, boolean enabled) { this.id = id; this.passwordHash = passwordHash; this.enabled = enabled; } ... }
密码编码器
public interface PasswordEncoder { String encode(String password); }
用户服务
public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; } public boolean isValidUser(String id, String password) { User user = userRepository.findById(id); return isEnabledUser(user) && isValidPassword(user, password); } private boolean isEnabledUser(User user) { return user != null && user.isEnabled(); } private boolean isValidPassword(User user, String password) { String encodedPassword = passwordEncoder.encode(password); return encodedPassword.equals(user.getPasswordHash()); } }

此示例代码可以在 GitHub 上找到,因此您可以下载它以与本文一起查看。

应用 Mockito

使用我们的示例代码,让我们看看如何应用 Mockito 并编写一些测试。

创建模拟

使用 Mockito,创建模拟就像调用静态方法Mockito.mock()一样简单:

 import static org.mockito.Mockito.*; ... PasswordEncoder passwordEncoder = mock(PasswordEncoder.class);

注意 Mockito 的静态导入。 对于本文的其余部分,我们将隐含地考虑添加此导入。

导入之后,我们模拟出PasswordEncoder ,一个接口。 Mockito 不仅模拟接口,还模拟抽象类和具体的非最终类。 开箱即用,Mockito 不能模拟最终类和最终或静态方法,但如果你真的需要它,Mockito 2 提供了实验性的 MockMaker 插件。

另请注意, equals()hashCode()方法不能被模拟。

创建间谍

要创建 spy,您需要调用 Mockito 的静态方法spy()并将其传递给一个实例以进行 spy。 调用返回对象的方法将调用真正的方法,除非这些方法被存根。 这些调用被记录下来,并且这些调用的事实可以被验证(参见verify()的进一步描述)。 让我们做一个间谍:

 DecimalFormat decimalFormat = spy(new DecimalFormat()); assertEquals("42", decimalFormat.format(42L));

创建间谍与创建模拟没有太大区别。 此外,所有用于配置 mock 的 Mockito 方法也适用于配置 spy。

与模拟相比,间谍很少使用,但您可能会发现它们对于测试无法重构的遗留代码很有用,其中测试它需要部分模拟。 在这些情况下,您可以创建一个 spy 并存根它的一些方法来获得您想要的行为。

默认返回值

调用mock(PasswordEncoder.class)返回PasswordEncoder的一个实例。 我们甚至可以调用它的方法,但是它们会返回什么? 默认情况下,mock 的所有方法都返回“未初始化”或“空”值,例如,数字类型(原始和装箱)为零,布尔值为 false,大多数其他类型为 null。

考虑以下接口:

 interface Demo { int getInt(); Integer getInteger(); double getDouble(); boolean getBoolean(); String getObject(); Collection<String> getCollection(); String[] getArray(); Stream<?> getStream(); Optional<?> getOptional(); }

现在考虑下面的代码片段,它给出了从模拟方法中期望的默认值的概念:

 Demo demo = mock(Demo.class); assertEquals(0, demo.getInt()); assertEquals(0, demo.getInteger().intValue()); assertEquals(0d, demo.getDouble(), 0d); assertFalse(demo.getBoolean()); assertNull(demo.getObject()); assertEquals(Collections.emptyList(), demo.getCollection()); assertNull(demo.getArray()); assertEquals(0L, demo.getStream().count()); assertFalse(demo.getOptional().isPresent());

存根方法

新鲜的、未更改的模拟仅在极少数情况下有用。 通常,我们想要配置 mock 并定义调用 mock 的特定方法时要做什么。 这称为存根

Mockito 提供了两种存根方法。 第一种方式是“这个方法被调用时,然后做一些事情”。 考虑以下代码段:

 when(passwordEncoder.encode("1")).thenReturn("a");

它读起来几乎像英语:“当passwordEncoder.encode(“1”)被调用时,返回一个a 。”

存根的第二种方式更像是“当使用以下参数调用此模拟方法时执行某些操作”。 这种存根的方式更难阅读,因为最后指定了原因。 考虑:

 doReturn("a").when(passwordEncoder).encode("1");

带有这种存根方法的代码片段将显示为:“当使用参数1调用passwordEncoderencode()方法时返回a 。”

第一种方式被认为是首选,因为它是类型安全的并且更易读。 然而,您很少被迫使用第二种方式,例如在对 spy 的真实方法进行 stub 时,因为调用它可能会产生不希望的副作用。

让我们简要探讨一下 Mockito 提供的 stubbing 方法。 我们将在示例中包含两种存根方法。

返回值

thenReturndoReturn()用于指定在方法调用时要返回的值。

 //”when this method is called, then do something” when(passwordEncoder.encode("1")).thenReturn("a");

要么

//”do something when this mock's method is called with the following arguments” doReturn("a").when(passwordEncoder).encode("1");

您还可以指定多个值,这些值将作为连续方法调用的结果返回。 最后一个值将用作所有进一步方法调用的结果。

 //when when(passwordEncoder.encode("1")).thenReturn("a", "b");

要么

//do doReturn("a", "b").when(passwordEncoder).encode("1");

使用以下代码段可以实现相同的目的:

 when(passwordEncoder.encode("1")) .thenReturn("a") .thenReturn("b");

此模式也可以与其他存根方法一起使用,以定义连续调用的结果。

返回自定义响应

then()thenAnswer()的别名和doAnswer()实现相同的目的,即设置一个自定义答案,以便在调用方法时返回,如下所示:

 when(passwordEncoder.encode("1")).thenAnswer( invocation -> invocation.getArgument(0) + "!");

要么

doAnswer(invocation -> invocation.getArgument(0) + "!") .when(passwordEncoder).encode("1");

thenAnswer()采用的唯一参数是Answer接口的实现。 它有一个带有InvocationOnMock类型参数的方法。

您还可以作为方法调用的结果引发异常:

 when(passwordEncoder.encode("1")).thenAnswer(invocation -> { throw new IllegalArgumentException(); });

…或者调用类的真实方法(不适用于接口):

 Date mock = mock(Date.class); doAnswer(InvocationOnMock::callRealMethod).when(mock).setTime(42); doAnswer(InvocationOnMock::callRealMethod).when(mock).getTime(); mock.setTime(42); assertEquals(42, mock.getTime());

如果您认为它看起来很麻烦,那您是对的。 Mockito 提供thenCallRealMethod()thenThrow()来简化测试的这方面。

调用真实方法

顾名思义, thenCallRealMethod()doCallRealMethod()调用模拟对象上的真实方法:

 Date mock = mock(Date.class); when(mock.getTime()).thenCallRealMethod(); doCallRealMethod().when(mock).setTime(42); mock.setTime(42); assertEquals(42, mock.getTime());

调用真实方法可能对部分模拟有用,但要确保被调用的方法没有不需要的副作用并且不依赖于对象状态。 如果是这样,间谍可能比模拟更适合。

如果你创建一个接口的模拟并尝试配置一个存根来调用一个真实的方法,Mockito 将抛出一个异常信息非常丰富的消息。 考虑以下代码段:

 when(passwordEncoder.encode("1")).thenCallRealMethod();

Mockito 将失败并显示以下消息:

 Cannot call abstract real method on java object! Calling real methods is only possible when mocking non abstract method. //correct example: when(mockOfConcreteClass.nonAbstractMethod()).thenCallRealMethod();

感谢 Mockito 开发人员提供如此详尽的描述!

抛出异常

thenThrow()doThrow()配置一个模拟方法来抛出异常:

 when(passwordEncoder.encode("1")).thenThrow(new IllegalArgumentException());

要么

doThrow(new IllegalArgumentException()).when(passwordEncoder).encode("1");

Mockito 确保抛出的异常对于该特定的存根方法有效,并且如果异常不在方法的检查异常列表中,则会抱怨。 考虑以下:

 when(passwordEncoder.encode("1")).thenThrow(new IOException());

会导致错误:

 org.mockito.exceptions.base.MockitoException: Checked exception is invalid for this method! Invalid: java.io.IOException

如您所见,Mockito 检测到encode()不能抛出IOException

您还可以传递异常的类而不是传递异常的实例:

 when(passwordEncoder.encode("1")).thenThrow(IllegalArgumentException.class);

要么

doThrow(IllegalArgumentException.class).when(passwordEncoder).encode("1");

也就是说,Mockito 无法以与验证异常实例相同的方式验证异常类,因此您必须遵守纪律,不要传递非法的类对象。 例如,以下将抛出IOException尽管encode()预计不会抛出检查异常:

 when(passwordEncoder.encode("1")).thenThrow(IOException.class); passwordEncoder.encode("1");

使用默认方法模拟接口

值得注意的是,在为接口创建 mock 时,Mockito 会模拟该接口的所有方法。 从 Java 8 开始,接口可能包含默认方法和抽象方法。 这些方法也是模拟的,因此您需要注意使它们充当默认方法。

考虑以下示例:

 interface AnInterface { default boolean isTrue() { return true; } } AnInterface mock = mock(AnInterface.class); assertFalse(mock.isTrue());

在此示例中, assertFalse()将成功。 如果这不是您所期望的,请确保您已经让 Mockito 调用了真正的方法,如下所示:

 AnInterface mock = mock(AnInterface.class); when(mock.isTrue()).thenCallRealMethod(); assertTrue(mock.isTrue());

参数匹配器

在前面的部分中,我们使用精确值作为参数配置了我们的模拟方法。 在这些情况下,Mockito 只是在内部调用equals()来检查预期值是否等于实际值。

但是,有时我们事先并不知道这些值。

也许我们只是不关心作为参数传递的实际值,或者我们想为更广泛的值定义一个反应。 所有这些场景(以及更多场景)都可以通过参数匹配器来解决。 这个想法很简单:不是提供一个精确的值,而是为 Mockito 提供一个参数匹配器来匹配方法参数。

考虑以下代码段:

 when(passwordEncoder.encode(anyString())).thenReturn("exact"); assertEquals("exact", passwordEncoder.encode("1")); assertEquals("exact", passwordEncoder.encode("abc"));

您可以看到,无论我们将什么值传递给encode() ,结果都是相同的,因为我们在第一行中使用了anyString()参数匹配器。 如果我们用简单的英语重写那行,听起来就像“当密码编码器被要求对任何字符串进行编码时,然后返回字符串'exact'。”

Mockito 要求您通过匹配器精确值提供所有参数。 因此,如果一个方法有多个参数,并且您只想对它的一些参数使用参数匹配器,那就忘了它。 你不能写这样的代码:

 abstract class AClass { public abstract boolean call(String s, int i); } AClass mock = mock(AClass.class); //This doesn't work. when(mock.call("a", anyInt())).thenReturn(true);

要修复错误,我们必须替换最后一行以包含aeq参数匹配器,如下所示:

 when(mock.call(eq("a"), anyInt())).thenReturn(true);

在这里,我们使用了eq()anyInt()参数匹配器,但还有许多其他可用的。 有关参数匹配器的完整列表,请参阅org.mockito.ArgumentMatchers类的文档。

请务必注意,您不能在验证或存根之外使用参数匹配器。 例如,您不能拥有以下内容:

 //this won't work String orMatcher = or(eq("a"), endsWith("b")); verify(mock).encode(orMatcher);

Mockito 将检测错位的参数匹配器并抛出InvalidUseOfMatchersException 。 应以这种方式使用参数匹配器进行验证:

 verify(mock).encode(or(eq("a"), endsWith("b")));

参数匹配器也不能用作返回值。 Mockito 不能返回anyString()或任何东西; 存根调用时需要一个准确的值。

自定义匹配器

当您需要提供一些 Mockito 中尚不可用的匹配逻辑时,自定义匹配器会派上用场。 创建自定义匹配器的决定不应掉以轻心,因为需要以非平凡的方式匹配参数表明设计存在问题或测试变得过于复杂。

因此,在编写自定义匹配器之前,有必要检查是否可以通过使用一些宽松的参数匹配器(例如isNull()nullable()来简化测试。 如果您仍然觉得需要编写参数匹配器,Mockito 提供了一系列方法来完成它。

考虑以下示例:

 FileFilter fileFilter = mock(FileFilter.class); ArgumentMatcher<File> hasLuck = file -> file.getName().endsWith("luck"); when(fileFilter.accept(argThat(hasLuck))).thenReturn(true); assertFalse(fileFilter.accept(new File("/deserve"))); assertTrue(fileFilter.accept(new File("/deserve/luck")));

在这里,我们创建了hasLuck参数匹配器,并使用argThat()将匹配器作为参数传递给模拟方法,如果文件名以“luck”结尾,则将其存根以返回true 。 您可以将ArgumentMatcher视为一个函数式接口,并使用 lambda 创建它的实例(这就是我们在示例中所做的)。 不太简洁的语法如下所示:

 ArgumentMatcher<File> hasLuck = new ArgumentMatcher<File>() { @Override public boolean matches(File file) { return file.getName().endsWith("luck"); } };

如果您需要创建一个适用于原始类型的参数匹配器,那么org.mockito.ArgumentMatchers中还有其他几种方法:

  • charThat(ArgumentMatcher<Character> 匹配器)
  • booleanThat(ArgumentMatcher<Boolean> 匹配器)
  • byteThat(ArgumentMatcher<Byte> 匹配器)
  • shortThat(ArgumentMatcher<Short> 匹配器)
  • intThat(ArgumentMatcher<Integer> 匹配器)
  • longThat(ArgumentMatcher<Long> 匹配器)
  • floatThat(ArgumentMatcher<Float> 匹配器)
  • doubleThat(ArgumentMatcher<Double> 匹配器)

组合匹配器

当条件太复杂而无法使用基本匹配器处理时,创建自定义参数匹配器并不总是值得的; 有时结合匹配器可以解决问题。 Mockito 提供参数匹配器来在匹配原始和非原始类型的参数匹配器上实现常见的逻辑操作('not'、'and'、'or')。 这些匹配器在org.mockito.AdditionalMatchers类中作为静态方法实现。

考虑以下示例:

 when(passwordEncoder.encode(or(eq("1"), contains("a")))).thenReturn("ok"); assertEquals("ok", passwordEncoder.encode("1")); assertEquals("ok", passwordEncoder.encode("123abc")); assertNull(passwordEncoder.encode("123"));

在这里,我们结合了两个参数匹配器的结果: eq("1")contains("a") 。 最后的表达式or(eq("1"), contains("a"))可以解释为“参数字符串必须等于“1”或包含“a”。

请注意,在org.mockito.AdditionalMatchers类中列出了不太常见的匹配器,例如geq()leq()gt()lt() ,它们是适用于原始值和java.lang.Comparable

验证行为

一旦使用了模拟或间谍,我们就可以verify发生了特定的交互。 从字面上看,我们是在说“嘿,Mockito,确保使用这些参数调用此方法。”

考虑以下人工示例:

 PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); when(passwordEncoder.encode("a")).thenReturn("1"); passwordEncoder.encode("a"); verify(passwordEncoder).encode("a");

在这里,我们设置了一个模拟并调用它的encode()方法。 最后一行验证了 mock 的encode()方法是用特定的参数值a调用的。 请注意,验证存根调用是多余的; 上一个片段的目的是展示在一些交互发生后进行验证的想法。

如果我们将最后一行更改为具有不同的参数(例如b ),则先前的测试将失败,并且 Mockito 将抱怨实际调用具有不同的参数( b而不是预期a )。

参数匹配器可用于验证,就像存根一样:

 verify(passwordEncoder).encode(anyString());

默认情况下,Mockito 会验证该方法是否被调用一次,但您可以验证任意数量的调用:

 // verify the exact number of invocations verify(passwordEncoder, times(42)).encode(anyString()); // verify that there was at least one invocation verify(passwordEncoder, atLeastOnce()).encode(anyString()); // verify that there were at least five invocations verify(passwordEncoder, atLeast(5)).encode(anyString()); // verify the maximum number of invocations verify(passwordEncoder, atMost(5)).encode(anyString()); // verify that it was the only invocation and // that there're no more unverified interactions verify(passwordEncoder, only()).encode(anyString()); // verify that there were no invocations verify(passwordEncoder, never()).encode(anyString());

verify()的一个很少使用的特性是它能够在超时时失败,这主要用于测试并发代码。 例如,如果我们的密码编码器在另一个线程中同时调用verify() ,我们可以编写如下测试:

 usePasswordEncoderInOtherThread(); verify(passwordEncoder, timeout(500)).encode("a");

如果在 500 毫秒或更短的时间内调用并完成encode() ,则此测试将成功。 如果您需要等待您指定的全部时间,请使用after()而不是timeout()

 verify(passwordEncoder, after(500)).encode("a");

其他验证模式( times()atLeast()等)可以与timeout()after()结合使用以进行更复杂的测试:

 // passes as soon as encode() has been called 3 times within 500 ms verify(passwordEncoder, timeout(500).times(3)).encode("a");

除了times() ,支持的验证模式还包括only()atLeast()atLeastOnce() (作为atLeast(1)的别名)。

Mockito 还允许您在一组模拟中验证调用顺序。 它不是一个经常使用的功能,但如果调用顺序很重要,它可能会很有用。 考虑以下示例:

 PasswordEncoder first = mock(PasswordEncoder.class); PasswordEncoder second = mock(PasswordEncoder.class); // simulate calls first.encode("f1"); second.encode("s1"); first.encode("f2"); // verify call order InOrder inOrder = inOrder(first, second); inOrder.verify(first).encode("f1"); inOrder.verify(second).encode("s1"); inOrder.verify(first).encode("f2");

如果我们重新排列模拟调用的顺序,测试将失败并显示VerificationInOrderFailure

也可以使用verifyZeroInteractions()来验证是否没有调用。 此方法接受一个或多个模拟作为参数,如果调用传入的模拟的任何方法,则该方法将失败。

还值得一提的是verifyNoMoreInteractions()方法,因为它将模拟作为参数,可用于检查对这些模拟的每个调用是否都经过验证。

捕获参数

除了验证使用特定参数调用方法之外,Mockito 还允许您捕获这些参数,以便以后可以在它们上运行自定义断言。 换句话说,你是在说“嘿,Mockito,验证这个方法是否被调用,并给我调用它的参数值。”

让我们创建一个PasswordEncoder的模拟,调用encode() ,捕获参数并检查它的值:

 PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); passwordEncoder.encode("password"); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder).encode(passwordCaptor.capture()); assertEquals("password", passwordCaptor.getValue());

如您所见,我们将passwordCaptor.capture()作为encode()的参数传递给验证; 这在内部创建了一个保存参数的参数匹配器。 然后我们使用passwordCaptor.getValue()检索捕获的值并使用assertEquals()检查它。

如果我们需要跨多个调用捕获参数, ArgumentCaptor允许您使用getAllValues()检索所有值,如下所示:

 PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); passwordEncoder.encode("password1"); passwordEncoder.encode("password2"); passwordEncoder.encode("password3"); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder, times(3)).encode(passwordCaptor.capture()); assertEquals(Arrays.asList("password1", "password2", "password3"), passwordCaptor.getAllValues());

相同的技术可用于捕获可变数量方法参数(也称为 varargs)。

测试我们的简单示例

现在我们对 Mockito 有了更多的了解,是时候回到我们的演示了。 让我们编写isValidUser方法测试。 下面是它的样子:

 public class UserServiceTest { private static final String PASSWORD = "password"; private static final User ENABLED_USER = new User("user id", "hash", true); private static final User DISABLED_USER = new User("disabled user id", "disabled user password hash", false); private UserRepository userRepository; private PasswordEncoder passwordEncoder; private UserService userService; @Before public void setup() { userRepository = createUserRepository(); passwordEncoder = createPasswordEncoder(); userService = new UserService(userRepository, passwordEncoder); } @Test public void shouldBeValidForValidCredentials() { boolean userIsValid = userService.isValidUser(ENABLED_USER.getId(), PASSWORD); assertTrue(userIsValid); // userRepository had to be used to find a user with verify(userRepository).findById(ENABLED_USER.getId()); // passwordEncoder had to be used to compute a hash of "password" verify(passwordEncoder).encode(PASSWORD); } @Test public void shouldBeInvalidForInvalidId() { boolean userIsValid = userService.isValidUser("invalid id", PASSWORD); assertFalse(userIsValid); InOrder inOrder = inOrder(userRepository, passwordEncoder); inOrder.verify(userRepository).findById("invalid id"); inOrder.verify(passwordEncoder, never()).encode(anyString()); } @Test public void shouldBeInvalidForInvalidPassword() { boolean userIsValid = userService.isValidUser(ENABLED_USER.getId(), "invalid"); assertFalse(userIsValid); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder).encode(passwordCaptor.capture()); assertEquals("invalid", passwordCaptor.getValue()); } @Test public void shouldBeInvalidForDisabledUser() { boolean userIsValid = userService.isValidUser(DISABLED_USER.getId(), PASSWORD); assertFalse(userIsValid); verify(userRepository).findById(DISABLED_USER.getId()); verifyZeroInteractions(passwordEncoder); } private PasswordEncoder createPasswordEncoder() { PasswordEncoder mock = mock(PasswordEncoder.class); when(mock.encode(anyString())).thenReturn("any password hash"); when(mock.encode(PASSWORD)).thenReturn(ENABLED_USER.getPasswordHash()); return mock; } private UserRepository createUserRepository() { UserRepository mock = mock(UserRepository.class); when(mock.findById(ENABLED_USER.getId())).thenReturn(ENABLED_USER); when(mock.findById(DISABLED_USER.getId())).thenReturn(DISABLED_USER); return mock; } }

深入了解 API

Mockito 提供了一个可读、方便的 API,但让我们探索它的一些内部工作原理,以了解它的局限性并避免奇怪的错误。

让我们检查运行以下代码段时 Mockito 内部发生了什么:

 // 1: create PasswordEncoder mock = mock(PasswordEncoder.class); // 2: stub when(mock.encode("a")).thenReturn("1"); // 3: act mock.encode("a"); // 4: verify verify(mock).encode(or(eq("a"), endsWith("b")));

显然,第一行创建了一个模拟。 Mockito 使用 ByteBuddy 创建给定类的子类。 新的类对象有一个生成的名称,如demo.mockito.PasswordEncoder$MockitoMock$1953422997 ,其equals()将用作检查身份,而hashCode()将返回身份哈希码。 生成并加载类后,将使用 Objenesis 创建其实例。

让我们看看下一行:

 when(mock.encode("a")).thenReturn("1");

顺序很重要:这里执行的第一条语句是mock.encode("a") ,它将在 mock 上调用encode() ,默认返回值为null 。 所以真的,我们将null作为when()的参数传递。 Mockito 并不关心传递给when()的确切值,因为它在调用该方法时将有关模拟方法调用的信息存储在所谓的“正在进行的存根”中。 稍后,当我们调用when() ,Mockito 会拉取正在进行的存根对象并将其作为when()的结果返回。 然后我们在返回的正在进行的存根对象上调用thenReturn(“1”)

第三行, mock.encode("a"); 很简单:我们正在调用存根方法。 在内部,Mockito 保存此调用以供进一步验证并返回存根调用答案; 在我们的例子中,它是字符串1

在第四行( verify(mock).encode(or(eq("a"), endsWith("b"))); ),我们要求 Mockito 验证是否调用了encode()那些具体论据。

verify()首先执行,这将 Mockito 的内部状态变为验证模式。 重要的是要了解 Mockito 将其状态保存在ThreadLocal中。 这使得实现良好的语法成为可能,但另一方面,如果框架使用不当(例如,如果您尝试在验证或存根之外使用参数匹配器),则会导致奇怪的行为。

那么 Mockito 是如何创建or匹配器的呢? 首先,调用eq("a") ,并将一个equals匹配器添加到匹配器堆栈中。 其次, endsWith("b") ,并将endsWith匹配器添加到堆栈中。 最后, or(null, null)被调用——它使用从堆栈中弹出的两个匹配器,创建or匹配器,并将其推入堆栈。 最后,调用encode() 。 然后,Mockito 验证该方法是否被调用了预期的次数并使用了预期的参数。

虽然参数匹配器不能被提取到变量中(因为它改变了调用顺序),但它们可以被提取到方法中。 这保留了调用顺序并使堆栈保持在正确的状态:

 verify(mock).encode(matchCondition()); … String matchCondition() { return or(eq("a"), endsWith("b")); }

更改默认答案

在前面的部分中,我们以这样一种方式创建了我们的模拟,即当调用任何模拟方法时,它们返回一个“空”值。 此行为是可配置的。 如果 Mockito 提供的那些不合适,您甚至可以提供自己的org.mockito.stubbing.Answer实现,但是当单元测试变得过于复杂时,这可能表明出现问题。 记住 KISS 原则!

让我们探索 Mockito 提供的预定义默认答案:

  • RETURNS_DEFAULTS是默认策略; 在设置模拟时不值得明确提及。

  • CALLS_REAL_METHODS使未存根的调用调用真实方法。

  • RETURNS_SMART_NULLS通过在使用未存根方法调用返回的对象时返回SmartNull而不是null来避免NullPointerException 。 您仍然会因NullPointerException失败,但SmartNull为您提供更好的堆栈跟踪,其中包含调用未存根方法的行。 这使得 RETURNS_SMART_NULLS 成为RETURNS_SMART_NULLS中的默认答案是值得的!

  • RETURNS_MOCKS首先尝试返回普通的“空”值,然后在可能的情况下进行模拟,否则返回null 。 空的标准与我们之前看到的有点不同:不是为字符串和数组返回null ,而是使用RETURNS_MOCKS创建的模拟分别返回空字符串和空数组。

  • RETURNS_SELF对于模拟构建器很有用。 使用此设置,如果调用的方法返回的类型等于被模拟类的类(或超类),则模拟将返回其自身的一个实例。

  • RETURNS_DEEP_STUBSRETURNS_MOCKS更深入,并创建能够从模拟等模拟返回模拟的模拟。与RETURNS_MOCKS ,空规则是RETURNS_DEEP_STUBS中的默认规则,因此它为字符串和数组返回null

 interface We { Are we(); } interface Are { So are(); } interface So { Deep so(); } interface Deep { boolean deep(); } ... We mock = mock(We.class, Mockito.RETURNS_DEEP_STUBS); when(mock.we().are().so().deep()).thenReturn(true); assertTrue(mock.we().are().so().deep());

命名模拟

Mockito 允许您命名一个模拟,如果您在测试中有很多模拟并且需要区分它们,这个功能很有用。 也就是说,需要命名模拟可能是设计不佳的症状。 考虑以下:

 PasswordEncoder robustPasswordEncoder = mock(PasswordEncoder.class); PasswordEncoder weakPasswordEncoder = mock(PasswordEncoder.class); verify(robustPasswordEncoder).encode(anyString());

Mockito 会抱怨,但是因为我们还没有正式命名 mocks,所以我们不知道是哪一个:

 Wanted but not invoked: passwordEncoder.encode(<any string>);

让我们通过在构造时传入一个字符串来命名它们:

 PasswordEncoder robustPasswordEncoder = mock(PasswordEncoder.class, "robustPasswordEncoder"); PasswordEncoder weakPasswordEncoder = mock(PasswordEncoder.class, "weakPasswordEncoder"); verify(robustPasswordEncoder).encode(anyString());

现在错误消息更加友好,并且清楚地指向了robustPasswordEncoder

 Wanted but not invoked: robustPasswordEncoder.encode(<any string>);

实现多个模拟接口

有时,您可能希望创建一个实现多个接口的模拟。 Mockito 能够轻松做到这一点,如下所示:

 PasswordEncoder mock = mock( PasswordEncoder.class, withSettings().extraInterfaces(List.class, Map.class)); assertTrue(mock instanceof List); assertTrue(mock instanceof Map);

Listening Invocations

A mock can be configured to call an invocation listener every time a method of the mock was called. Inside the listener, you can find out whether the invocation produced a value or if an exception was thrown.

 InvocationListener invocationListener = new InvocationListener() { @Override public void reportInvocation(MethodInvocationReport report) { if (report.threwException()) { Throwable throwable = report.getThrowable(); // do something with throwable throwable.printStackTrace(); } else { Object returnedValue = report.getReturnedValue(); // do something with returnedValue System.out.println(returnedValue); } } }; PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().invocationListeners(invocationListener)); passwordEncoder.encode("1");

In this example, we're dumping either the returned value or a stack trace to a system output stream. Our implementation does roughly the same as Mockito's org.mockito.internal.debugging.VerboseMockInvocationLogger (don't use this directly, it's internal stuff). If logging invocations is the only feature you need from the listener, then Mockito provides a cleaner way to express your intent with the verboseLogging() setting:

 PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().verboseLogging());

Take notice, though, that Mockito will call the listeners even when you're stubbing methods. 考虑以下示例:

 PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().verboseLogging()); // listeners are called upon encode() invocation when(passwordEncoder.encode("1")).thenReturn("encoded1"); passwordEncoder.encode("1"); passwordEncoder.encode("2");

This snippet will produce an output similar to the following:

 ############ Logging method invocation #1 on mock/spy ######## passwordEncoder.encode("1"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:85) has returned: "null" ############ Logging method invocation #2 on mock/spy ######## stubbed: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:85) passwordEncoder.encode("1"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:89) has returned: "encoded1" (java.lang.String) ############ Logging method invocation #3 on mock/spy ######## passwordEncoder.encode("2"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:90) has returned: "null"

Note that the first logged invocation corresponds to calling encode() while stubbing it. It's the next invocation that corresponds to calling the stubbed method.

其他设置

Mockito offers a few more settings that let you do the following:

  • Enable mock serialization by using withSettings().serializable() .
  • Turn off recording of method invocations to save memory (this will make verification impossible) by using withSettings().stubOnly() .
  • Use the constructor of a mock when creating its instance by using withSettings().useConstructor() . When mocking inner non-static classes, add an outerInstance() setting, like so: withSettings().useConstructor().outerInstance(outerObject) .

If you need to create a spy with custom settings (such as a custom name), there's a spiedInstance() setting, so that Mockito will create a spy on the instance you provide, like so:

 UserService userService = new UserService( mock(UserRepository.class), mock(PasswordEncoder.class)); UserService userServiceMock = mock( UserService.class, withSettings().spiedInstance(userService).name("coolService"));

When a spied instance is specified, Mockito will create a new instance and populate its non-static fields with values from the original object. That's why it's important to use the returned instance: Only its method calls can be stubbed and verified.

Note that, when you create a spy, you're basically creating a mock that calls real methods:

 // creating a spy this way... spy(userService); // ... is a shorthand for mock(UserService.class, withSettings() .spiedInstance(userService) .defaultAnswer(CALLS_REAL_METHODS));

When Mockito Tastes Bad

It's our bad habits that make our tests complex and unmaintainable, not Mockito. For example, you may feel the need to mock everything. This kind of thinking leads to testing mocks instead of production code. Mocking third-party APIs can also be dangerous due to potential changes in that API that can break the tests.

Though bad taste is a matter of perception, Mockito provides a few controversial features that can make your tests less maintainable. Sometimes stubbing isn't trivial, or an abuse of dependency injection can make recreating mocks for each test difficult, unreasonable or inefficient.

Clearing Invocations

Mockito allows for clearing invocations for mocks while preserving stubbing, like so:

 PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); UserRepository userRepository = mock(UserRepository.class); // use mocks passwordEncoder.encode(null); userRepository.findById(null); // clear clearInvocations(passwordEncoder, userRepository); // succeeds because invocations were cleared verifyZeroInteractions(passwordEncoder, userRepository);

Resort to clearing invocations only if recreating a mock would lead to significant overhead or if a configured mock is provided by a dependency injection framework and stubbing is non-trivial.

Resetting a Mock

Resetting a mock with reset() is another controversial feature and should be used in extremely rare cases, like when a mock is injected by a container and you can't recreate it for each test.

Overusing Verify

Another bad habit is trying to replace every assert with Mockito's verify() . It's important to clearly understand what is being tested: interactions between collaborators can be checked with verify() , while confirming the observable results of an executed action is done with asserts.

Mockito Is about Frame of Mind

Using Mockito is not just a matter of adding another dependency, it requires changing how you think about your unit tests while removing a lot of boilerplate.

With multiple mock interfaces, listening invocations, matchers and argument captors, we've seen how Mockito makes your tests cleaner and easier to understand, but like any tool, it must be used appropriately to be useful. Now armed with the knowledge of Mockito's inner workings, you can take your unit testing to the next level.