JMockit 无法模拟 class 的多个实例

JMockit unable to mock more than one instance of class

我想创建一个 Map,它将 Strings 作为其键,并将 class 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: Invalid attempt to get uninitialized instance of class com.urlservice.data.Candidate from stateless mockup at ...

我阅读了文档并尝试按以下方式使用 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; }
};

结果很奇怪 - 第一个 Candidate 被正确模拟,而其余被模拟的候选人根本没有被模拟,而是调用了他们的真实方法。

我试图恢复到 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: Already mocked: class com.urlservice.data.Candidate at...

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

更新:我没能在 1.28 之前的任何版本中重现这个问题,所以我想这是引入它的版本。

此外,关于我的第二个示例 (new MockUp(T targetInstance)),我查看了 class 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 的构造函数之前实例化它这一事实充其量也是有问题的。创建 mock 的全部意义不就是不需要实例化它吗?

在仔细检查了 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());
    }
}

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

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

如果 JMockit 的贡献者之一可以阐明这个问题,我们将不胜感激。