【问题标题】:JMockit unable to mock more than one instance of classJMockit 无法模拟多个类的实例
【发布时间】:2016-10-12 01:44:40
【问题描述】:

我想创建一个 Map,它将 Strings 作为其键,并将 Candidate 类的模拟实例作为其值。

    Map<String, Long> domainNameToId = new HashMap<String, Long>();
    domainNameToId.put("farmaciapuentezurita.es", 1234l);
    domainNameToId.put("vivefarma.com", 2345l);
    domainNameToId.put("eurofarmacia.com", 3456l);

    Map<String, Candidate> expectedCandidates = new HashMap<String, Candidate>();
    for(String domain : domainNameToId.keySet()) {
        final Candidate cand = new MockUp<Candidate>() {
            @Mock Long getDomainId() { return domainNameToId.get(domain); } // private method
            @Mock boolean validateAndPrepare() { return true; }
            @Mock String getRepresentingName() { return domain; }
        }.getMockInstance();
        expectedCandidates.put(domain, cand);
    }

上述代码在将 JMockit 从 1.20 升级到 1.28 之前可以正常工作

现在我得到一个例外:

java.lang.IllegalStateException:从无状态模型中获取 com.urlservice.data.Candidate 类的未初始化实例的尝试无效 在...

我阅读了文档并尝试以以下方式使用new MockUp(T targetInstance)(这是循环的主体):

final Candidate cand = new Candidate(domain);
new MockUp<Candidate>(cand) {
    @Mock Long getDomainId() { return domainNameToId.get(domain); }  // private method
    @Mock boolean validateAndPrepare() { return true; }
    @Mock String getRepresentingName() { return domain; }
};

结果很奇怪——第一个候选者被正确地模拟了,而其余被模拟的候选者根本没有被模拟,它们的真实方法被调用了。

我尝试恢复到Expectations API:

final Candidate cand = new Candidate(domain);
new Expectations(cand) {{
    cand.getDomainId(); result = domainNameToId.get(domain);  // Had to make it public :-(
    cand.validateAndPrepare(); result = true;
    cand.getRepresentingName(); result = domain;
}};

无济于事:

java.lang.IllegalArgumentException:已经模拟:com.urlservice.data.Candidate 类 在...

我真的想升级到最新版本,但我找不到解决此问题的方法。

更新:我没有设法在 1.28 之前的任何版本中重现此问题,所以我猜这是引入它的版本。

此外,与我的第二个示例 (new MockUp(T targetInstance)) 相关,我查看了类 MockUp 第 402 行的源代码,在我看来,预期的行为是不模拟除第一个:

  MockUp<?> previousMockUp = findPreviouslyFakedClassIfMockUpAlreadyApplied();

  if (previousMockUp != null) {
     targetType = previousMockUp.targetType;
     mockedClass = previousMockUp.mockedClass;
     return;  // Input param targetInstance is disregarded
  }

我错过了什么?

UPDATE2:我想出了一个失败的测试示例。这有点麻烦,但我相信它会明白这一点。

public class SampleTest {

class TestedClass {
    private IncrementingDependency dep;
    TestedClass(IncrementingDependency dep) { this.dep = dep; }
    public int getVal() { return dep.inc(); }
}

class IncrementingDependency {
    int val;
    public IncrementingDependency(int val) { this.val = val; }
    public int inc() { return ++val; }
}

@Test
public void sampleTest() {
    List<Integer> inputVals = Arrays.asList(1, 2, 3);
    List<TestedClass> incrementingClasses = new ArrayList<TestedClass>();

    for (Integer num : inputVals) {
        IncrementingDependency dep = new IncrementingDependency(num);
        new MockUp<IncrementingDependency>(dep) {
            @Mock int inc() { return num; }  // Mock with different behavior - DON'T INCREMENT
        };
        incrementingClasses.add(new TestedClass(dep));
    }

    assertThat(incrementingClasses.get(0).getVal()).isEqualTo(1); // Passes - 1 wasn't incremented (mocked behavior)
    assertThat(incrementingClasses.get(1).getVal()).isEqualTo(2); // Fails - real code was called and 2 was incremented to 3
    assertThat(incrementingClasses.get(2).getVal()).isEqualTo(3); // We never get to this point
}
}

请注意,即使这个示例不会失败,我需要在将依赖项传递给MockUp 的构造函数之前实例化我的依赖项这一事实充其量是有问题的。创建一个模拟的全部意义不是你不需要实例化它吗?

【问题讨论】:

  • 使用new MockUp&lt;Candidate&gt;(cand) 时无法重现“奇怪”的结果。它似乎适用于 1.28。你能展示一个失败的示例测试吗?
  • 请注意 API documentation for Mock&lt;T&gt;(T) 表示“仅影响给定实例”。因此,在Candidate 的任何其他 实例上调用的方法都不会转到@Mock 方法。
  • @Rogério 当然,这正是我想要实现的。我会尽快提供一个失败的示例测试,但我最后放置的 JMockit 代码示例是否强烈支持我的主张?
  • 失败的示例测试在 JMockit 1.20 中也失败了;这部分从 1.20 到 1.28 没有任何变化。另外,我不明白测试的重点:为什么要模拟IncrementingDependency?为什么要“模仿不同的行为”?
  • 请注意模拟(和假货)的全部意义一般是它们“模仿”或“模仿”实际(预期)行为模拟/伪造的依赖项,但没有执行实际的实现。您不会将模拟用于不同的行为。

标签: java unit-testing junit mocking jmockit


【解决方案1】:

在仔细检查了 JMockit 的实际项目单元测试(非常清晰和有条理)之后,我设法以一种有点奇怪的方式解决了这个问题:

private void expectCandidatesFromMap(final Map<String, Long> domainNameToId) {
    Map<String, Candidate> expectedCandidates = new HashMap<String, Candidate>();

    class MockedCandidate extends MockUp<Candidate> {
        private final String domainName;
        private final Long domainId;

        MockedCandidate(String domainName) {
            this.domainName = domainName;
            this.domainId = domainNameToId.get(domainName);
        }

        @Mock Long getDomainId() { return domainId; }
        @Mock String getRepresentingName() { return domainName; }
        @Mock boolean validateAndPrepare() { return true; }
    }

    for (String domain : domainNameToId.keySet()) {
        expectedCandidates.put(domain, new MockedCandidate(domain).getMockInstance());
    }
}

即使我的测试现在通过了,我也不完全理解为什么模拟类的特定实例的唯一方法是创建另一个临时类(而不是像我之前习惯的那样匿名内联它这个版本)。

这种方法需要多几行代码(私有字段声明、ctor 实现),但可以说更优雅。

如果 JMockit 的一位贡献者能对此事有所了解,我们将不胜感激。

【讨论】:

  • 其实这不是唯一的方法(参见Invocation 类)。并且没有必要像上面那样创建命名模型;它只需要一个“有状态”模型(带有实例字段),它仍然可以用匿名子类编写。真正的问题是使用不同的关联模拟实例多次应用相同的模型类。这不受支持,因为它需要 JMockit 维护从模拟实例到模拟实例的内部映射。这在技术上是可行的,但不符合 API 的预期用途。
  • 所以基本上我在这里所做的就是通过命名模型绕过 API 的预期用途?您能否详细说明为什么它不是预期用途?
  • Mock-ups 旨在提供 fake 实现(主要是类的),而不是创建“数据”对象的便捷方式,几乎没有复杂/昂贵行为。相反,测试应该在它需要的尽可能多的实例中实例化具有所需状态的类。通常不会嘲笑或伪造所述实例。
  • 那么创建“数据”对象的预期便捷方法是什么?很少或没有复杂/昂贵的行为?如果在许多人编写的大型代码库的约束下,用所需状态实例化类是复杂的和/或昂贵的和/或实际上不可能的,其中一些不具有与此相同的理念怎么办?跨度>
  • 您的问题中没有任何内容表明创建 Candidate 对象是禁止的。事实上,它甚至显示了带有new Candidate(domain) 的一行。我不会推测没有显示代码的想象情况。
猜你喜欢
  • 1970-01-01
  • 2017-05-24
  • 2014-12-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多