Can I mock a superclass's constructor with Moc

2019-04-07 00:30发布

问题:

Is it possible using Mockito and optionally Powermock to mock a superclass S such that any calls to the superclass to S (including calls to the S() constructor) are mocked? So using the below example, if I replace S with MockS using Mockito, will the call to super() use the constructor in MockS?

class S {
   S() {
      // Format user's hard drive, call 911, and initiate self-destruct
   }
}

class T extends S {
   T() {
      super();
   }
}

class Test {
   @Mock private S mockS;
   new T(); // T's call to super() should call the mock, not the destructive S.
}

I've seen questions about mocking individual methods in S or mocking only calls to super(), and read that this is unsupported, but it's not clear whether or not I can mock the entire superclass.

With my current tests, when I try to mock S, T's call to super() calls the real implementation, not the mock.

回答1:

To work around this apparent limitation, I refactored my code, replacing inheritance with delegation, and I think I've ended up with a better design anyhow since the inheritance wasn't really necessary.

The new code looks like this. Mind you the code for the question was simplified, so the real classes have much more functionality.

class S {
   S() {
      // Format user's hard drive, call 911, and initiate self-destruct
   }
}

class T {
   T(S s) {} // Now T "has an S" instead of "is an S"
}

class Test {
   @Mock private S mockS;
   new T(s); // T's call to super() should call the mock, not the destructive S.
}

For those interested, using Guice and Android, the test looks more like this:

class T {
   T(Activity activity, S s) {}
}

class Test {
  @Mock Activity activity;
  @Mock S mockS;
  injector = Guice.createInjector(new AbstractModule() {
     @Override protected void configure() {
        bind(Activity.class).toInstance(activity);
        bind(S.class).toInstance(mockS);
     }}
  );
  T t = injector.getInstance(T.class);
}


回答2:

I think this is possible with PowerMock only if the method on the child is different from the method on the superclass (i.e., you cannot mock the parent method if the child overrides that method). For a little more detail, you can look at the relevant bug report.

For PowerMock, check out Suppressing Unwanted Behavior page to see if it will be enough for your needs.


After much digging around, I ended up using JMockit for these tricky cases. Before I moved on to JMockit, I tried stubbing out all the places exceptions were thrown using suppression. In the end, I needed to override some methods, and not just suppress them, so I ended up abandoning it.

Example usage for Android case:

First, you mock out your superclass using the @MockClass annotation:

@MockClass(realClass = Activity.class, instantiation = PerMockedInstance)
public class FakeActivity {
    public Bundle mSavedInstanceState;

    @Mock
    public void $init() {}

    @Mock
    public void onCreate(Bundle savedInstanceState) {
        mSavedInstanceState = savedInstanceState;
    }
}

When activated, this class will replace the default constructor of Activity with $init(), and replace the onCreate method with the one above. WIth android, the unit under test is derived from Activity (in my sample code, it is HelloTestActivity). The test class looks like this:

public class HelloTestActivityTest3 extends AndroidTest {
    @Tested
    HelloTestActivity activity;

    FakeActivity fakeActivity = new FakeActivity();

    @Before
    public void setupMocks()
    {
        Mockit.setUpMock(fakeActivity);
    }

    @Test
    public void onCreate_bundle(@Mocked Bundle savedInstanceState)
    {
        // Try to access out-of-band information from the fake
        activity.onCreate(savedInstanceState);
        assertSame(savedInstanceState, fakeActivity.mSavedInstanceState);
    }
}

The call Mockit.setupMock(fakeActivity) replaces the super class with my instance of the fake. With this usage, you can access internal state of your fake class as well. If you don't need to override any methods with custom functionality, you can use other methods available from Mockit class.

As rogerio pointed out in the comments below, mocking the Activity class is the bare minimum. The following code demonstrates this.

public class HelloTestActivityTest4 {
    @Tested
    HelloTestActivity activity;

    @Mocked
    Activity base;

    @Test
    public void testOnCreate() throws Exception {
        // Just make sure "Stub!" exception is not thrown.
        activity.onCreate(null);
    }
}

The declaration @Mocked Activity base; causes all methods (excepting static initializers) of Activity class and its superclasses to be mocked in the tests defined in HelloActivityTest4.



回答3:

What you can do is extract the 'dangerous' code in your superclass constructor into a non-private method, then use Mockito spy on your class T and override the behaviour in that extracted method.

This would of course violate encapsulation. Guava offers the VisibleForTesting annotation for such cases.