Skip to content

Commit

Permalink
Issue #97: Add stubbing of static methods
Browse files Browse the repository at this point in the history
Similar to stubbing of instance methods this uses an JVMTI agent to add a
method entry hook to all affected methods. This change adds a new JVMTI
agent that is only used for stubbing of static methods. This comes with
its own mockMaker that hooks into the multiplexer added in a previous
change.

Most aspects of stubbing static methods are quite similar to instance
methods. Here are the differences:

== Scope of mocking ==

For instance methods the methods stay stubbed until the object goes
away. For static methods there is no object. Hence from the user point
of view the scope is defined by a MockitoSession. Internally we use a
marker object to track all stubbing of static methods for a class.

We use ThreadLocal variables to communicate between the frontEnd i.e.
ExtendedMockito and the backend i.e. InlineStaticMockMaker.

== Highlander rule for static mocking ===

As we set up mocking/spying per class, we can only mock/spy a class once
per session.

== 'inherited' static methods ==

In the following case:

---
class SuperClass {
    static String returnA() { return "A"; }
}

class SubClass {
}

@test
public void test() {
    SubClass.returnA();
}
---

The backtraces actually contains SuperClass.returnA(). We have to look
at the byte code of the calling method to figure out if the method was
called on the super or subclass. While this is _super_expensive_ (~10ms
in my unscientific experiments) I see no other way.
  • Loading branch information
moltmann authored and drewhannay committed Apr 21, 2018
1 parent 50be449 commit 75313ad
Show file tree
Hide file tree
Showing 16 changed files with 2,577 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,23 @@

package com.android.dx.mockito.inline.extended.tests;

import com.android.dx.mockito.inline.extended.StaticMockitoSession;

import org.junit.Test;
import org.mockito.Mock;

import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.eq;

public class MockInstanceUsingExtendedMockito {
@Mock
private TestClass mockField;

public static class TestClass {
public String echo(String arg) {
return arg;
Expand All @@ -43,4 +50,19 @@ public void mockClass() throws Exception {
verify(t).echo("mocked");
verify(t).echo("stubbed");
}

@Test
public void useMockitoSession() throws Exception {
StaticMockitoSession session = mockitoSession().initMocks(this).startMocking();
try {
assertNull(mockField.echo("mocked"));

when(mockField.echo(eq("stubbed"))).thenReturn("B");
assertEquals("B", mockField.echo("stubbed"));
verify(mockField).echo("mocked");
verify(mockField).echo("stubbed");
} finally {
session.finishMocking();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.android.dx.mockito.inline.extended.tests;

import android.content.ContentResolver;
import android.provider.Settings;
import android.support.test.InstrumentationRegistry;

import org.junit.Test;
import org.mockito.MockingDetails;
import org.mockito.MockitoSession;
import org.mockito.exceptions.misusing.MissingMethodInvocationException;
import org.mockito.quality.Strictness;

import static android.provider.Settings.Global.DEVICE_NAME;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.clearInvocations;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockingDetails;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.reset;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.staticMockMarker;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verifyZeroInteractions;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;

public class MockStatic {
private static class SuperClass {
final String returnA() {
return "superA";
}

static String returnB() {
return "superB";
}

static String returnC() {
return "superC";
}
}

private static final class SubClass extends SuperClass {
static String recorded = null;

static String returnC() {
return "subC";
}

static final String record(String toRecord) {
recorded = toRecord;
return "record";
}
}

@Test
public void spyStatic() throws Exception {
ContentResolver resolver = InstrumentationRegistry.getTargetContext().getContentResolver();
String deviceName = Settings.Global.getString(resolver, DEVICE_NAME);

MockitoSession session = mockitoSession().spyStatic(Settings.Global.class).startMocking();
try {
// Make sure non-mocked methods work as before
assertEquals(deviceName, Settings.Global.getString(resolver, DEVICE_NAME));
} finally {
session.finishMocking();
}
}

@Test
public void mockStatic() throws Exception {
ContentResolver resolver = InstrumentationRegistry.getTargetContext().getContentResolver();
String deviceName = Settings.Global.getString(resolver, DEVICE_NAME);

MockitoSession session = mockitoSession().mockStatic(Settings.Global.class).startMocking();
try {
// By default all static methods of the mocked class should return null/0/false
assertNull(Settings.Global.getString(resolver, DEVICE_NAME));

when(Settings.Global.getString(any(ContentResolver.class), eq(DEVICE_NAME)))
.thenReturn("This is a test");

// Make sure behavior is changed
assertEquals("This is a test", Settings.Global.getString(resolver, DEVICE_NAME));
} finally {
session.finishMocking();
}

// Once the mocking is removed, the behavior should be back to normal
assertEquals(deviceName, Settings.Global.getString(resolver, DEVICE_NAME));
}

@Test
public void mockOverriddenStaticMethod() throws Exception {
MockitoSession session = mockitoSession().mockStatic(SubClass.class).startMocking();
try {
// By default all static methods of the mocked class should return the default answers
assertNull(SubClass.returnB());
assertNull(SubClass.returnC());

// Super class is not mocked
assertEquals("superB", SuperClass.returnB());
assertEquals("superC", SuperClass.returnC());

when(SubClass.returnB()).thenReturn("fakeB");
when(SubClass.returnC()).thenReturn("fakeC");

// Make sure behavior is changed
assertEquals("fakeB", SubClass.returnB());
assertEquals("fakeC", SubClass.returnC());

// Super class should not be affected
assertEquals("superB", SuperClass.returnB());
assertEquals("superC", SuperClass.returnC());
} finally {
session.finishMocking();
}

// Mocking should be stopped
assertEquals("superB", SubClass.returnB());
assertEquals("subC", SubClass.returnC());
}

@Test
public void mockSuperMethod() throws Exception {
MockitoSession session = mockitoSession().mockStatic(SuperClass.class).startMocking();
try {
// By default all static methods of the mocked class should return the default answers
assertNull(SuperClass.returnB());
assertNull(SuperClass.returnC());

// Sub class should not be affected
assertEquals("superB", SubClass.returnB());
assertEquals("subC", SubClass.returnC());

when(SuperClass.returnB()).thenReturn("fakeB");
when(SuperClass.returnC()).thenReturn("fakeC");

// Make sure behavior is changed
assertEquals("fakeB", SuperClass.returnB());
assertEquals("fakeC", SuperClass.returnC());

// Sub class should not be affected
assertEquals("superB", SubClass.returnB());
assertEquals("subC", SubClass.returnC());
} finally {
session.finishMocking();
}

// Mocking should be stopped
assertEquals("superB", SuperClass.returnB());
assertEquals("superC", SuperClass.returnC());
}

@Test(expected = MissingMethodInvocationException.class)
public void nonMockedTest() throws Exception {
when(SuperClass.returnB()).thenReturn("fakeB");
}

@Test
public void resetMock() throws Exception {
MockitoSession session = mockitoSession().mockStatic(SuperClass.class).startMocking();
try {
assertNull(SuperClass.returnB());

when(SuperClass.returnB()).thenReturn("fakeB");
assertEquals("fakeB", SuperClass.returnB());

reset(staticMockMarker(SuperClass.class));
assertNull(SuperClass.returnB());
} finally {
session.finishMocking();
}
}

@Test
public void resetSpy() throws Exception {
MockitoSession session = mockitoSession().spyStatic(SuperClass.class).startMocking();
try {
assertEquals("superB", SuperClass.returnB());

when(SuperClass.returnB()).thenReturn("fakeB");
assertEquals("fakeB", SuperClass.returnB());

reset(staticMockMarker(SuperClass.class));
assertEquals("superB", SuperClass.returnB());
} finally {
session.finishMocking();
}
}

@Test
public void staticMockingIsSeparateFromNonStaticMocking() throws Exception {
SuperClass objA = new SuperClass();
SuperClass objB;

MockitoSession session = mockitoSession().mockStatic(SuperClass.class).startMocking();
try {
assertNull(SuperClass.returnB());
assertNull(objA.returnB());

objB = mock(SuperClass.class);

assertEquals("superA", objA.returnA());

// Any kind of static method method call should be mocked
assertNull(objB.returnA());

assertNull(SuperClass.returnB());
assertNull(objA.returnB());
assertNull(objB.returnB());
} finally {
session.finishMocking();
}

assertEquals("superA", objA.returnA());
assertNull(objB.returnA());

// Any kind of static method method call should _not_ be mocked
assertEquals("superB", SuperClass.returnB());
assertEquals("superB", objA.returnB());
assertEquals("superB", objB.returnB());
}

@Test
public void mockWithTwoClasses() throws Exception {
MockitoSession session = mockitoSession().mockStatic(SuperClass.class)
.mockStatic(SubClass.class).startMocking();
try {
when(SuperClass.returnB()).thenReturn("fakeB");
assertEquals("fakeB", SuperClass.returnB());

when(SubClass.returnC()).thenReturn("fakeC");
assertEquals("fakeC", SubClass.returnC());
} finally {
session.finishMocking();
}
}

@Test
public void clearInvocationsRemovedInvocations() throws Exception {
MockitoSession session = mockitoSession().mockStatic(SuperClass.class).startMocking();
try {
SuperClass.returnB();
clearInvocations(staticMockMarker(SuperClass.class));
verifyZeroInteractions(staticMockMarker(SuperClass.class));
} finally {
session.finishMocking();
}
}

@Test
public void verifyMockingDetails() throws Exception {
MockitoSession session = mockitoSession().mockStatic(SuperClass.class)
.spyStatic(SubClass.class).startMocking();
try {
when(SuperClass.returnB()).thenReturn("fakeB");
SuperClass.returnB();
SuperClass.returnC();

MockingDetails superClassDetails = mockingDetails(staticMockMarker(SuperClass.class));
assertTrue(superClassDetails.isMock());
assertFalse(superClassDetails.isSpy());
assertEquals(2, superClassDetails.getInvocations().size());
assertEquals(1, superClassDetails.getStubbings().size());

MockingDetails subClassDetails = mockingDetails(staticMockMarker(SubClass.class));
assertTrue(subClassDetails.isMock());
assertTrue(subClassDetails.isSpy());
} finally {
session.finishMocking();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.android.dx.mockito.inline.extended.tests;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class StaticMockitoSessionVsMockitoJUnitRunner {
@Test
public void simpleStubbing() throws Exception {
(new MockStatic()).spyStatic();
}
}
Loading

0 comments on commit 75313ad

Please sign in to comment.