【问题标题】:@InjectMocks behaving differently with Java 6 and 7@InjectMocks 在 Java 6 和 7 中表现不同
【发布时间】:2025-12-16 01:40:02
【问题描述】:

通过一个非常简单的 Mockito 运行 JUnit 测试和类,当使用 Java 1.6.0_32 和 Java 1.7.0_04 运行测试时,我看到了不同的输出,并且想了解为什么会发生这种情况。我怀疑正在进行某种类型擦除,但希望得到明确的答案。

这是我的示例代码以及如何从命令行运行的说明:

FooServiceTest.java

import org.junit.*;
import org.junit.runner.*;
import org.mockito.*;
import org.mockito.runners.MockitoJUnitRunner;
import static org.mockito.Mockito.*;
import java.util.*;

@RunWith(MockitoJUnitRunner.class)
public class FooServiceTest {
  @Mock Map<String, String> mockStringString;
  @Mock Map<String, Integer> mockStringInteger;

  @InjectMocks FooService fooService;

  public static void main(String[] args) {
    new JUnitCore().run(FooServiceTest.class);
  }

  @Before
  public void setup() {
    MockitoAnnotations.initMocks(this);
  }

  @Test
  public void checkInjection() {
    when(mockStringString.get("foo")).thenReturn("bar");
    fooService.println();
  }
}

FooService.java

import java.util.*;

public class FooService {
  private Map<String, String> stringString = new HashMap<String, String>();
  private Map<String, Integer> stringInteger = new HashMap<String, Integer>();

  public void println() {
    System.out.println(stringString.get("foo") + " " + stringInteger);
  }
}

编译和运行这个例子:

  • 将以上内容保存到文件中
  • 下载并放在同一目录下junit.4.10.jarmockito-all-1.9.0.jar
  • 设置 PATH 以包含 JDK
  • javac -cp junit-4.10.jar;mockito-all-1.9.0.jar *.java编译
  • 使用java -cp .;junit-4.10.jar;mockito-all-1.9.0.jar FooServiceTest 运行

我相信上面的输出是null {},因为@InjectMocks 字段注入无法正确解析类型,因为它们都是Map 类型。 正确吗

现在更改一个模拟名称以匹配类中的字段应该允许 Mockito 找到匹配项。比如改变

@Mock Map<String, Integer> mockStringInteger;

@Mock Map<String, Integer> stringInteger;

然后使用 Java 1.6.0_32 编译/运行会给出(恕我直言)输出bar stringInteger,但使用 1.7.0_04 会给出null stringInteger

这是我运行它的方式(从 Windows 7 中的命令行):

E:\src\mockito-test>set PATH="C:\Program Files (x86)\Java\jdk1.6.0_32\bin"
E:\src\mockito-test>javac -cp junit-4.10.jar;mockito-all-1.9.0.jar *.java
E:\src\mockito-test>java -cp .;junit-4.10.jar;mockito-all-1.9.0.jar FooServiceTest
    bar stringInteger
E:\src\mockito-test>set PATH="C:\Program Files (x86)\Java\jdk1.7.0_04\bin"
E:\src\mockito-test>javac -cp junit-4.10.jar;mockito-all-1.9.0.jar *.java
E:\src\mockito-test>java -cp .;junit-4.10.jar;mockito-all-1.9.0.jar FooServiceTest
    null stringInteger

【问题讨论】:

    标签: java mockito java-7 type-erasure java-6


    【解决方案1】:

    我相信上面的输出是 I null {} 因为@InjectMocks 字段注入无法正确解析类型,因为它们都是 Map 类型。这是正确的吗?

    是的,在这些字段上正确 Mockito 无法清除歧义,因此它只是忽略了这些歧义的字段。

    通过一个非常简单的 Mockito 运行 JUnit 测试和类,当使用 Java 1.6.0_32 和 Java 1.7.0_04 运行测试时,我看到了不同的输出,并且想了解为什么会发生这种情况。

    实际上,差异在于 Arrays.sort 的不同行为,因此 JDK 6 和 JDK 7 之间的 Collections.sort()。差异在于新算法应该减少 20% 的交换。大概就是这个swap操作让JDK6和JDK7下都能正常运行吧。

    如果您只重命名具有相同类型(或相同擦除)的字段中的一个模拟字段,请允许您“自找麻烦”。当不能按类型区分模拟时,您确实应该将所有模拟字段命名为相应的字段,但 Javadoc 并没有明确说明。

    顺便说一下,非常感谢您报告这种奇怪的行为,我创建了一个issue on Mockito,但是现在我不会真正解决这个问题,而是确保跨 JDK 的行为相同。解决这种情况可能需要在保持兼容性的同时编写新的算法,同时您应该根据测试类的字段命名所有字段模拟。

    现在要做的事情可能是通过额外的比较来调整比较器,以在 JDK6 和 JDK7 上强制执行相同的顺序。另外在 Javadoc 中添加了一些警告。

    编辑:对大多数人来说,两次通过可能会解决问题。

    希望对您有所帮助。感谢您发现问题。


    顺便说一句,您需要MockitoAnnotations.initMocks(this); 或跑步者@RunWith(MockitoJUnitRunner.class),两者都不是必需的,甚至可能会导致一些问题。 :)

    【讨论】:

    • 是的,这就是我要找的。我意识到我有点在找麻烦,但JDK6 和JDK7 之间的区别最近是一个真正的问题。感谢您的澄清,不要同时使用initMocks()@RunWith 并正式提出问题的提示。
    • @andyb 该问题已在主干中修复 :)
    【解决方案2】:

    Mockito 的行为是未定义的,如果有多个模拟匹配将要注入的字段之一。在这里,“匹配”意味着它是正确的类型,忽略任何类型参数 - 类型擦除阻止 Mockito 知道类型参数。因此,在您的示例中,两个模拟中的任何一个都可以注入两个字段中的任何一个。

    您已经设法观察到 Java 6 与 Java 7 的不同行为这一事实有点牵强附会。在这两个 Java 版本中,没有理由期望 Mockito 能够在 mockStringStringmockStringInteger 之间正确选择它注入的两个字段之一。

    【讨论】:

    • 我很好奇,它在文档中说“Mockito 尝试按类型注入(在类型相同的情况下使用名称)。”为什么不用这里的名字来打破两个同类型字段的关系呢?
    • 嗯,这可能是文档中的错误。我知道 InjectMocks 的行为在 1.9.0 中发生了一些变化,也许我们同时忘记了更改文档。我将与 Mockito 团队的其他成员讨论它,看看我是否能找出发生了什么。在这种情况下,它应该使用该名称,这似乎是合理的;但我检查了代码,我很确定它没有。我会及时通知你我的发现。
    • 抱歉,出现了一些问题,我无法再看这个了。感谢您的回答。具有讽刺意味的是,当我第一次在一些真实代码中发现这一点时,却发生了相反的情况。 Java 6 编译/运行代码未按预期注入,Java 7 等效代码正在运行。
    • 说清楚,我现在再看一遍。在我之前的评论中应该说“直到现在再看这个”:-)
    • 其实这个测试没有类型擦除。完整的类型信息可通过 ASM 和反射 API 获得。如果 Mockito 不使用它,那么它就是一个错误。
    【解决方案3】:

    这是正确的吗?

    确实,由于type erasure,Mockito 在运行时/通过反射无法看到各种 Map 之间的差异,这让 Mockito 很难进行正确的注入。

    nullstringString.get("foo") 的响应可能有两个原因:

    1. stringString 已正确模拟,但没有发生存根(get 始终返回 null)。
    2. stringString 没有被模拟,所以仍然是一个没有"foo" 值的HashMap,所以get 将返回null

    {}stringInteger 变量的响应意味着它已被初始化(在类中)使用实际(空)HashMap

    所以你的输出告诉你stringInteger 没有被嘲笑。 stringString呢?

    如果两个@Mocks 中没有一个名称与被测类的任何字段匹配,则不会模拟任何内容。原因?我怀疑它无法决定注入哪个字段,所以它不会做任何模拟。您可以通过显示两个变量来验证这一点,这两个变量都会产生{}。这解释了您的 null 值。

    如果@Mocks 之一有一个匹配的名称,而另一个有一个不匹配的名称(您的修改,一个mockedStringString 和一个stringInteger,即具有相同的名称),应该Mockito 做什么?

    你希望它做的只是注入其中一个,并且只在具有相应名称的字段中注入。在您的情况下,您有mockedStringString(您希望这不匹配)和stringInteger(您希望它匹配)。由于您将不匹配的 mockedStringString (!) 存根,预期结果将是 null

    换句话说,对于给出的具体示例,我认为 Java 7 的响应是可以的,而 Java 6 的响应是不行的。

    要查看 Java 6 的(意外)行为发生了什么,请尝试只使用一个 @Mock -- 如果我正确模拟 stringString 并且 没有 模拟 @ 987654347@,stringString 的模拟被注入stringInteger 字段。换句话说,Mockito 似乎首先弄清楚它可以注入(给定名称),然后将模拟注入到匹配的可能性之一(但不一定是正确的)。

    【讨论】:

    • 感谢您的解释。我也在本地尝试了一个@Mock。我了解(并已阅读文档)mockito 在尝试注入模拟时可能会在幕后做什么,但我仍然不相信为什么 Java 6 和 7 的行为不同。您说您认为 Java 7 响应还可以,但我认为这是错误的,应该被注入并且真的想了解为什么。我意识到我可以用更好的模拟对象名称来解决问题,但这不是重点:-)
    • 为什么我认为它是正确的:在您修改后的示例代码中,您有stringIntegermockStringString。因此 stringString 应该在任何实现中找到(但stringInteger 应该!),这就是为什么我认为null 是一个好的答案。
    • 但是:如果您的编辑是将 mockedStringString 替换为匹配的 stringString 并保持其余部分不变,我希望会发生模拟并显示 bar。跨度>
    • 你说得对:我只有对你问题的这是正确的部分的答案。对这里的 Java 6 和 Java 行为之间的区别同样好奇,但我不知道。我无法轻易重现它(我在 Eclipse 中尝试使用不同的 JRE 版本)。
    最近更新 更多