@InjectMocks 在 Java 6 和 7 中的行为不同

11

我使用一个非常简单的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
3个回答

5
我认为上面的输出是I null {},因为@InjectMocks字段注入无法正确解析类型,因为它们都是Map类型。这个正确吗?
是的,在这些字段模拟对象无法清除歧义时,Mockito无法正确处理,所以它只忽略了这些有歧义的字段。
通过一个非常简单的Mockito运行JUnit测试和类,我发现当使用Java 1.6.0_32和Java 1.7.0_04运行测试时会看到不同的输出,并希望了解为什么会发生这种情况。
实际上,JDK 6和JDK 7之间Arrays.sort和Collections.sort()的不同行为导致了差异。差异在于新算法应该执行20%的交换操作更少。这可能就是在JDK6和JDK7下使事情正常工作的交换操作。
如果我可以说,如果您仅重命名具有相同类型(或相同擦除)的字段中的一个模拟字段,则会“自找麻烦”。当模拟对象无法通过类型区分时,您确实应将所有模拟字段命名为相应的字段,但Javadoc没有明确说明这一点。

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

现在要做的事情可能是通过添加其他比较来调整比较器以强制在JDK6和JDK7上具有相同的顺序。再加上在Javadoc中添加一些警告。

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

希望这能帮到您。感谢发现这个问题。


顺便提一下,您需要使用MockitoAnnotations.initMocks(this);@RunWith(MockitoJUnitRunner.class)运行器之一,使用两者都不是必需的,甚至可能会引起一些问题。 :)

是的,这正是我在寻找的。我意识到我有点自找麻烦,但最近JDK6和JDK7之间的差异确实是一个真正的问题。感谢您的澄清,不使用initMocks()@RunWith两者并用的提示,并正式提出了这个问题。 - andyb
2
@andyb 这个问题已经在主干代码中修复了 :) - bric3

2
Mockito的行为在存在多个匹配要注入的字段之一的mock对象时是不确定的。在这里,“匹配”意味着它是正确的类型,忽略任何类型参数 - 类型擦除使Mockito无法知道类型参数。因此,在您的示例中,这两个mock中的任何一个都可以注入到这两个字段中。
你在Java 6和Java 7中观察到不同行为的事实有点牵强附会。在任何一个Java版本中,都没有理由期望Mockito在注入的两个字段中为mockStringString或mockStringInteger之一进行正确的选择。

1
我很好奇,文档中写道:“Mockito 尝试按类型注入(在类型相同的情况下使用名称进行区分)。” 为什么它不在这里使用名称来打破两个相同类型字段之间的关系? - Tom Tresansky
2
嗯,这可能是文档中的错误。我知道InjectMocks的行为在1.9.0中有所改变,也许我们忘记同时更改文档了。我将与Mockito团队讨论此事,并查明发生了什么。在这种情况下,它应该使用名称似乎是合理的,但我已经检查了代码,我相当确定它没有这样做。我会及时告诉你我的发现。 - Dawood ibn Kareem
抱歉,有些事情出现了,我一直没有能够再次查看这个。感谢您的答案。讽刺的是,当我第一次在某些实际代码中发现这种情况时,相反的情况发生了。Java 6编译/运行的代码没有按预期注入,而Java 7等效的代码却可以运行。 - andyb
要明确一点,我现在再次查看它。在我的先前评论中应该说“直到现在都没有再看过这个” :-) - andyb
实际上,在这个测试中没有类型擦除。完整的类型信息可以通过ASM和反射API获得。如果Mockito没有使用它,那么这是一个错误。 - Rogério

0

这个正确吗

确实,由于类型擦除,Mockito在运行时/通过反射无法区分不同的Map,这使得Mockito很难进行适当的注入。

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

  1. stringString被正确模拟,但未进行存根操作(get始终返回null)。
  2. stringString未被模拟,因此仍然是一个没有值为"foo"HashMap,因此get将返回null

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

所以你的输出告诉你的是stringInteger没有被模拟。那么stringString呢?

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

如果其中一个@Mock具有匹配的名称,而另一个没有(您的修改,一个mockedStringString和一个stringInteger,即具有相同名称),那么Mockito应该怎么做呢?

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

换句话说,对于给定的特定示例,我认为Java 7的响应是可以的,而Java 6的响应则不可以。

为了查看Java 6的(意外)行为,尝试只使用一个@Mock -- 如果我正确地模拟了stringString并且没有对stringInteger进行模拟,则stringString的模拟将被注入到stringInteger字段中。换句话说,Mockito似乎首先确定它可以注入(根据名称),然后将模拟注入到匹配可能性之一(但不一定是正确的可能性)。


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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接