【问题标题】:How should I mock Arrays.asList in Java Unit Test?我应该如何在 Java 单元测试中模拟 Arrays.asList?
【发布时间】:2021-11-22 04:48:54
【问题描述】:

我在 Java 应用程序中有一个单元测试,但在执行以下行时出现错误:

when(Arrays.asList(Locale.getISOCountries()).contains(countryCode)).thenReturn(true);

错误信息中没有提到 mocking,但我认为问题与 not mocking 有关 Arrays.asList(Locale.getISOCountries()when 方法的一部分。那么,我应该如何对待这条线呢?通常我会模拟服务和存储库,但我不确定我是否应该像@Mock Locale locale; 那样模拟这部分。我试过了,没用。

这是错误信息:

org.mockito.exceptions.misusing.WrongTypeOfReturnValue: 
Boolean cannot be returned by toString()
toString() should return String

注意:我也尝试了doReturn(true).when(Arrays.asList(Locale.getISOCountries()).contains(countryCode));,但没有成功。

【问题讨论】:

  • 为什么Locale.getISOCountries()会包含GB,给定方法“返回ISO 3166中定义的所有2字母国家代码的列表”?你认为你在这里用模拟设置了什么?
  • 也许如果你包含失败测试的堆栈跟踪和此类测试的相关代码而不是那个设置行,你会得到一个有用的答案。
  • @RubioRic 谢谢朋友,我补充说。有什么帮助吗?
  • @Alex Locale.getISOCountries()始终包含“GB”,因为这是 ISO3166-2 中定义的国家/地区代码。鉴于此,您根本不需要做 任何事情 来确保您的测试以该行为运行。删除线。放下态度,你是在这里寻求帮助的人。
  • "并且可能不包含该值" Locale.getISOCountries() 总是返回相同的列表。它不会返回包含任意字符串的数组。您不应该尝试让测试执行在生产代码中永远不会实际发生的事情,因为那样您是在测试测试,而不是实际行为。

标签: java unit-testing testing mocking mockito


【解决方案1】:

TL;DR:不要试图嘲笑这个。一方面,你不能;另一方面,你不应该。

最好使用默认行为。删除您尝试使用 Mockito 配置此行为的行。

如果您坚持需要能够使用任意国家/地区列表,请参阅下面的“依赖注入”部分,注意提到的警告。


试图模拟Arrays.asList(Locale.getISOCountries()).contains(countryCode) 的返回值意味着您试图改变Arrays.asList 返回的List<String> 在将特定String[] 作为参数时的行为。

你不能使用 Mockito,因为它不是魔法:它不允许替换任意表达式的行为。

when(aMock.method(123)).thenReturn("hello") 之类的工作方式是模拟对象 - aMock - 记录 method 是使用参数 123 调用的。此信息被推送到堆栈中,when 方法可以从中检索并处理它。

只有在aMock 是 Mockito 创建的对象时才会执行入栈操作:Mockito 实现接口的方法/覆盖类的方法以进行此记录。

Arrays.asList(Locale.getISOCountries()).contains("GB") 中涉及的所有对象都不是由 Mockito 创建的。因此,这些对象都没有捕获调用并将其压入堆栈的方法; Mockito 基本上看不到正在发生任何事情,所以当when 调用时,它只是使用 Mockito 中的任何状态。显示的错误信息:

org.mockito.exceptions.misusing.WrongTypeOfReturnValue: 
Boolean cannot be returned by toString()
toString() should return String

表示它认为您正在配置toString 方法的行为,显然与Arrays.asListLocale.getISOCountries()List.contains 无关。

出于同样的原因,您不能模拟 Arrays.asList(Locale.getISOCountries())Locale.getISOCountries() 的返回值:它们只是 Mockito 不知道的对象。

所以,这就是为什么这种模拟不起作用。关于为什么你一开始不想这样做:


List.contains has specific semantics,即当且仅当参数在列表中时它才返回 true。这有一些含义,例如 aList.contains(o) == true 暗示对 aList.indexOf(o) 的调用将返回一个非负值。

这意味着嘲笑contains的后果是:

  1. 您还必须配置列表中其他方法的模拟,以使列表的行为与List.contains 的结果一致(因此,indexOfsubListiterator 等)- 和,如果您将等于 o 的元素设置为其他值(因为 Arrays.asList 允许这样做),模拟应该如何表现;

  2. 您没有配置其他模拟,并且您的 List 的行为与实际的 List 不一致,这将对您的代码行为产生不可预知的影响。

但是您实际上不必担心执行 1.(这很好,因为基本上不可能正确执行,例如列表的副本具有相同的 contains 行为)或 2.(这很好,因为引入损坏的List 的不可预测性简直是个坏主意):Arrays.asList 有一个完美工作的List 接口实现;您只需确保传入的参数(在本例中为 Locale.getISOCountries())包含您想要的元素。

模拟Arrays.asList(...).contains 的返回值既没必要也不可取。


因此,现在的问题从确保Arrays.asList(Locale.getISOCountries()).contains(countryCode) 之一转移到确保Locale.getISOCountries() 至少有一个等于countryCode 的元素。

the Javadoc of Locale.getISOCountries()中所述:

返回 ISO 3166 中定义的所有 2 字母国家/地区代码列表

鉴于 GB (as was originally asked) 是 ISO 3166 中定义的 2 个字母的国家/地区代码,它将始终(或至少在 ISO3166 更改之前)是 Locale.getISOCountries() 返回的数组的元素。

因此,Locale.getISOCountries() 返回的数组将包含"GB",因此Arrays.asList(Locale.getISOCountries()).contains("GB"),实际上并没有做任何事情。


但是对于任意的countryCode,将Arrays.asList(Locale.getISOCountries()).contains(countryCode) 设置为true 存在问题。根据上面的论点,您只想通过确保Locale.getISOCountries() 具有等于countryCode 的元素来实现这一点。

如果 countryCode 是另一个由两个字母组成的 ISO-3166-2 国家/地区代码,那么这已经可以工作了,就像在 GB 的情况下一样。

如果countryCode 不是 ISO 国家/地区代码,那么如果该方法被记录为仅返回 ISO-3166-2 国家/地区代码,那么您绝对不应该让方法返回它,因为这不会在生产代码中发生.


不应将模拟用作在测试中做任意事情的一种方式。

您只需要一个测试来测试可以在生产代码中实际发生的事情。理想情况下,您使用“真实的东西”; test doubles(其中一个模拟是一种类型)只有在使用“真实的东西”很困难时才会发挥作用,因为真实的东西很慢,昂贵,难以重现(例如网络错误或磁盘满等错误情况)等等。但是,测试替身应该只做你真正看到的事情。

因此,即使您可以模拟 Locale.getISOCountries() 以确保它返回一个包含非 ISO-3166-2 countryCode 的数组,您也不应该,因为这实际上永远不会在生产中发生;并且测试在生产中无法发生的事情的测试在告诉您有用的事情方面的价值非常有限。

实际上,您可以使用PowerMock 模拟Locale.getISOCountries() 等静态方法;但是改变静态方法的行为是非常不可取的,因为它不仅会改变你的行为——它还会改变任何调用它的人。因此,行为可能会产生意想不到的后果,无论是在附近还是在代码的其余部分。

例如:


when(Arrays.asList(Locale.getISOCountries())).thenReturn(Collections.singletonList(countryCode.toUpperCase(Locale.ENGLISH)));

除了更改返回列表的可变性语义(Arrays.asList 允许 setCollections.singletonList 不允许)之外,它现在与 Locale 类中的其他国家代码返回方法不一致(例如getISOCountryCodes(Type). 追查并修复所有这些不一致几乎是不可能的。

如果我们可以使用 PowerMock 来模拟 Arrays.asList(Locale.getISOCountries()) 的返回值,即 Locale.getISOCountries() 方法的一个不太通用的用例会怎么样?这仍然存在意外后果的问题 - 程序的其他部分可能会调用 Arrays.asList(Locale.getISOCountries()),而模拟行为是不可取的。

如果我们可以使用 PowerMock 来模拟 一个特定调用Arrays.asList(Locale.getISOCountries()) 的返回值会怎样?这很脆弱,例如,如果添加了另一个调用,则必须确保测试已正确更新,否则调用之间的行为将不一致。

没有赢得 PowerMock 战斗的好方法。


这里有很多词,但关键是尝试不恰当地使用模拟会产生相当难以处理的后果:使用实际行为(没有模拟)是最好的;但是,如果必须使用模拟,则它的行为方式不应是真实代码永远不会的。

幸运的是,Mockito 阻止了你这样做;但希望这个答案能够彻底解释为什么它首先是错误的方法。


依赖注入

综上所述,有一种方法可以让您的代码在面对任意国家代码时也能正常工作:dependency injection

虽然有很多 DI 框架(例如 Guice、Spring)引入了很多功能(以及复杂性和恐怖性),但依赖注入只是意味着:将事物作为参数传递。

例如,如果您希望 Arrays.asList(Locale.getISOCountries()).contains(countryCode) 为真的代码出现在方法中,请将国家/地区列表作为参数注入该方法:

class MyClass {
  void myMethod(List<String> countryCodes, String countryCode) {
    if (countryCodes.contains(countryCode)) {
      // ...
    }
  }
}

或将其设为构造函数参数:

class MyClass {
  private final List<String> countryCodes;

  MyClass(List<String> countryCodes) {
    // Defensive copy.
    this.countryCodes = Collections.unmodifiableList(new ArrayList<>(countryCodes));
  }

  void myMethod(String countryCode) {
    if (countryCodes.contains(countryCode)) {
      // ...
    }
  }
}

在你的生产代码中,传入Arrays.asList(Locale.getISOCountries());在测试代​​码中,传入你喜欢的任何列表。

但仍然:注意此代码与在测试中直接使用Locale.getISOCountries() 和相关方法的代码之间的交互。如果存在此类交互的风险,使用静态 Local.getISOCountries() 编写测试仍然更安全。

【讨论】:

  • 我的意思是when(Arrays.asList(Locale.getISOCountries()).contains("XYZ")).thenReturn(true);
  • 它在返回 false 时抛出异常。那么,我应该如何模拟必要的价值呢?或者我应该如何检查这条线?
  • 朋友???请问有什么回复吗?
  • 我也试过when(Arrays.asList(Locale.getISOCountries())).thenReturn(Collections.singletonList(countryCode.toUpperCase(Locale.ENGLISH)));
猜你喜欢
  • 1970-01-01
  • 2017-01-29
  • 1970-01-01
  • 1970-01-01
  • 2021-06-18
  • 1970-01-01
  • 2018-06-29
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多