Can't use @Mocked on Base classes anymore #236

Closed
rolger opened this Issue Nov 3, 2015 · 7 comments

Projects

None yet

2 participants

@rolger
rolger commented Nov 3, 2015

I encountered an issue (?) with JMockit 1.20. Until 1.19 I was able to annoted a Base class with @Mocked and I could record some behaviour. Since 1.20 the behaviorz changed. The object is mocked but instead of returning the values recorded in the Expectations it provides just the default values.

public final class MockingBaseClassTest {
static class Base {
int getIntValue() { return 1; }
}

static class Bar extends Base {
    int doSomething() { return 1 + getIntValue(); }
}

@Test
public void failsInJMockit120(@Mocked final Base foo) throws Exception {
    new Expectations() {{
            foo.getIntValue(); result = 9;
    }};
    // the result in 1.20
    Assert.assertEquals(9, new Bar().doSomething());
    // the result in 1.19
    Assert.assertEquals(10, new Bar().doSomething());
}

}

Hopefully this is clear enough. Thanks in advance.

@rliesenfeld rliesenfeld added the question label Nov 3, 2015
@rliesenfeld
Member

Yes, indeed. This change was made to resolve issue #209.

@rolger
rolger commented Nov 5, 2015

I thought that recording an expectation will replace the implementation in the class it was implemented originally. Now it seems to support some kind of polymorphism.

So the question is, which implementation will be mocked if there is no code available in the inherited class? The example in #209 illustrates it well, it's creating a new implementation in the sub class.

Therefore I implemented some new tests and it seems to prove my understanding. So my questions is this understandig correct?

It's starts becoming more complex if the method is overwritten in an inherited class and that's my issue.

public final class MockingParentClassTest {
    private static final boolean CALL_ORIGINAL_IMPLEMENTATION = false;
    private static final boolean CALL_PARENT_IMPLEMENTATION = true;

    static class Parent  {  
        String getValue(boolean condition) { return "parent";  }
    }
    static class First extends Parent {
        @Override
        String getValue(boolean condition) { return condition ? "sub" :  super.getValue(condition); }
    }
    static class Second extends Parent { }

    @Test
    public void calls_the_original_implementation(@Mocked final Parent parent) throws Exception {
        new Expectations() {{
            parent.getValue(anyBoolean); result = "mockedParent";
        }};

        assertEquals(new Parent().getValue(CALL_ORIGINAL_IMPLEMENTATION), "mockedParent");
        assertEquals(new First().getValue(CALL_ORIGINAL_IMPLEMENTATION), "sub");
        assertEquals(new Second().getValue(CALL_ORIGINAL_IMPLEMENTATION), "mockedParent");
    }

    @Test
    public void calls_the_original_implementation(@Mocked final First first) throws Exception {
        new Expectations() {{
            first.getValue(anyBoolean); result = "mockedFirst";
        }};

        assertEquals(new Parent().getValue(CALL_ORIGINAL_IMPLEMENTATION), "parent");
        assertEquals(new First().getValue(CALL_ORIGINAL_IMPLEMENTATION), "mockedFirst");
        assertEquals(new Second().getValue(CALL_ORIGINAL_IMPLEMENTATION), "parent");
    }

    @Test
    public void calls_the_original_implementation(@Mocked final Second second) throws Exception {
        new Expectations() {{
            second.getValue(anyBoolean); result = "mockedSecond";
        }};

        assertEquals(new First().getValue(CALL_ORIGINAL_IMPLEMENTATION), "sub");
        assertEquals(new Second().getValue(CALL_ORIGINAL_IMPLEMENTATION), "mockedSecond");
        assertEquals(new Parent().getValue(CALL_ORIGINAL_IMPLEMENTATION), "parent");
    }

    @Test
    public void calls_the_parent_implementation(@Mocked final Parent parent) throws Exception {
        new Expectations() {{
            parent.getValue(anyBoolean); result = "mockedParent";
        }};

        assertEquals(new Parent().getValue(CALL_PARENT_IMPLEMENTATION), "mockedParent");
        assertEquals(new Second().getValue(CALL_PARENT_IMPLEMENTATION), "mockedParent");
        assertEquals(new First().getValue(CALL_PARENT_IMPLEMENTATION), "mockedParent");
    }
}

Depending on how I assign the annotation to a class, I'd expect different results:

  • @Mocked Parent: the original implementation in the base class Parent will be mocked and replaced.
  • @Mocked First: the original implementation in the sub class First will be mocked and replaced.
  • @Mocked Second: a new implementation in the base class Second will be recorded.

Everything is correct until the call is triggered from a sub class which is not mocked. The second and third assert of the test "calls_the_parent_implementation" fails with 1.20. It's seems that the class Parent is still mocked anyway but, why is it returning the default values of an mocked instance? I'd never expect this at all. How can I solve that?

By the way we have a lot of such occurances in our legacy code base, so it's really a pain for us. Currently about 20% of our Unit Tests are failing.

@rliesenfeld
Member

In JMockit's own test suites (about 1800 JUnit/TestNG tests), only a few needed adaptation after the change for issue #209.

So, what I really need to understand is what's the real-world situation(s) where one needs to mock a base/parent class with @Mocked?

@rolger
rolger commented Nov 5, 2015

I'll try to describe it but I'd need more time. Basicly the problem is the Legacy Code and the framework we are using. The code consists huge inheritance trees and we need to mock those parent classes to avoid that this code is included in the tests of the sub classes.

I'll try to figure out and document which pattern is the real problem in our code.

@rliesenfeld
Member

After some analysis, I think there is a change that should be made, as shown in the following test:

public final class MockingBaseClassTest {
    static class Base { String doIt() { return "base"; } }
    static class Subclass extends Base { }
    static class WithSuper extends Base { @Override String doIt() { return super.doIt(); } }

    @Test
    public void mockingAllInstancesOfABaseClass(@Mocked final Base mock) {
        new Expectations() {{ mock.doIt(); result = "mocked"; }};

        assertEquals("mocked", new Base().doIt());      // already ok (passing)
        assertEquals("base",   new Subclass().doIt());   // fails as getValue() returns null
        assertEquals("base",   new WithSuper().doIt()); // fails as getValue() returns null
    }
}

It stands to reason that doIt() should only exhibit mocked behavior on instances of the class that was declared to be @Mocked, not on any other classes (either sub- or super- classes). I will make this change and see which existing tests it breaks.

@rolger
rolger commented Nov 10, 2015

Our Legacy Code framework is based on

  • Eclipse RCP
  • inheritance providing a lot of helper funtionality
  • generics and generic methods
  • a lot of initialisation code in constructors

By using the @Mocked annotation on the base classes we are able to break this dependencies and to record the behaviour we need to run our Unit Tests. Especially in the case of generic methods it's necessary to record an expectation.

Further we don't want to retest the inherited behaviour of the framewwork. The idea is to handle it like any other dependency so we expect that the base classes provide an expected behaviour like a contract or any other API.

I know there are two workaround to solve the problem in our tests both based on partial mocking: either by passing the SUT to the expectation block or using the MockUp<> API. The disadvantage is that we would need to mock nearly all framework methods including the different constructors. This is quite complex and troublesome.

@rliesenfeld
Member

I will restore original mocking semantics when an expectation is recorded on a mocked base class, but replayed on a subclass instance. So, once a class is declared to be mocked, any subclass instance will behave according to recorded expectations; a type check will be made to ensure the replayed instance is the same class of the recorded instance, or a subclass.

@rliesenfeld rliesenfeld added enhancement and removed question labels Nov 16, 2015
@rliesenfeld rliesenfeld self-assigned this Nov 16, 2015
@rliesenfeld rliesenfeld added a commit that closed this issue Nov 23, 2015
@rliesenfeld rliesenfeld Restored mocking behavior for @Mocked base classes, when replaying in…
…vocations on a subclass instance; closes #236.
bae833f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment