Skip to content

Commit

Permalink
Mockito.mockStatic can take over from PowerMock now
Browse files Browse the repository at this point in the history
The mockito dependency upgrade is actually a big feature switch, so
the previous usage of PowerMock can be removed and mockito can do all
of our static mocking
  • Loading branch information
mikehardy committed Jul 16, 2020
1 parent f8548a7 commit bb742f2
Show file tree
Hide file tree
Showing 7 changed files with 158 additions and 140 deletions.
5 changes: 1 addition & 4 deletions AnkiDroid/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -254,10 +254,7 @@ dependencies {
api project(":api")

testImplementation 'org.junit.vintage:junit-vintage-engine:5.6.2'
testImplementation 'org.mockito:mockito-core:3.3.3'
testImplementation 'org.powermock:powermock-core:2.0.7'
testImplementation 'org.powermock:powermock-module-junit4:2.0.7'
testImplementation 'org.powermock:powermock-api-mockito2:2.0.7'
testImplementation 'org.mockito:mockito-inline:3.4.0'
testImplementation 'org.hamcrest:hamcrest-all:1.3'
testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
testImplementation "org.robolectric:robolectric:4.3.1"
Expand Down
138 changes: 71 additions & 67 deletions AnkiDroid/src/test/java/com/ichi2/anki/AnalyticsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,19 @@
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockedStatic;
import org.mockito.MockitoAnnotations;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

@PowerMockIgnore("javax.net.ssl.*")
@PrepareForTest({PreferenceManager.class, GoogleAnalytics.class})
@RunWith(PowerMockRunner.class)

import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.validateMockitoUsage;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

public class AnalyticsTest {

@Mock
Expand All @@ -57,96 +57,100 @@ public class AnalyticsTest {
@Mock
private SharedPreferences.Editor mMockSharedPreferencesEditor;

// This is actually a Mockito Spy of GoogleAnalyticsImpl
private GoogleAnalytics mAnalytics;

@Before
public void setUp() {
PowerMockito.mockStatic(PreferenceManager.class);
PowerMockito.mockStatic(GoogleAnalytics.class);
MockitoAnnotations.openMocks(this);

MockitoAnnotations.initMocks(this);

Mockito.when(mMockResources.getBoolean(R.bool.ga_anonymizeIp))
when(mMockResources.getBoolean(R.bool.ga_anonymizeIp))
.thenReturn(true);
Mockito.when(mMockResources.getInteger(R.integer.ga_sampleFrequency))
when(mMockResources.getInteger(R.integer.ga_sampleFrequency))
.thenReturn(10);
Mockito.when(mMockContext.getResources())
when(mMockContext.getResources())
.thenReturn(mMockResources);

Mockito.when(mMockContext.getString(R.string.ga_trackingId))
when(mMockContext.getString(R.string.ga_trackingId))
.thenReturn("Mock Tracking ID");
Mockito.when(mMockContext.getString(R.string.app_name))
when(mMockContext.getString(R.string.app_name))
.thenReturn("Mock Application Name");
Mockito.when(mMockContext.getPackageName())
when(mMockContext.getPackageName())
.thenReturn("mock_context");
Mockito.when(mMockContext.getSharedPreferences("mock_context_preferences", Context.MODE_PRIVATE))
when(mMockContext.getSharedPreferences("mock_context_preferences", Context.MODE_PRIVATE))
.thenReturn(mMockSharedPreferences);

Mockito.when(mMockSharedPreferences.getBoolean(UsageAnalytics.ANALYTICS_OPTIN_KEY, false))
when(mMockSharedPreferences.getBoolean(UsageAnalytics.ANALYTICS_OPTIN_KEY, false))
.thenReturn(true);
Mockito.when(PreferenceManager.getDefaultSharedPreferences(ArgumentMatchers.any()))
.thenReturn(mMockSharedPreferences);


Mockito.when(mMockSharedPreferencesEditor.putBoolean(UsageAnalytics.ANALYTICS_OPTIN_KEY, true))
when(mMockSharedPreferencesEditor.putBoolean(UsageAnalytics.ANALYTICS_OPTIN_KEY, true))
.thenReturn(mMockSharedPreferencesEditor);

Mockito.when(mMockSharedPreferences.edit())
when(mMockSharedPreferences.edit())
.thenReturn(mMockSharedPreferencesEditor);

Mockito.when(GoogleAnalytics.builder())
.thenReturn(new SpyGoogleAnalyticsBuilder());

mAnalytics = UsageAnalytics.initialize(mMockContext);
}


private static class SpyGoogleAnalyticsBuilder extends GoogleAnalyticsBuilder {
public GoogleAnalytics build() {
GoogleAnalytics analytics = super.build();
return Mockito.spy(analytics);
return spy(analytics);
}
}


@After
public void validate() {
Mockito.validateMockitoUsage();
validateMockitoUsage();
}


@Test
public void testSendException() {

// no root cause
Exception exception = Mockito.mock(Exception.class);
Mockito.when(exception.getCause()).thenReturn(null);
Throwable cause = UsageAnalytics.getCause(exception);
Mockito.verify(exception).getCause();
Assert.assertEquals(exception, cause);

// a 3-exception chain inside the actual analytics call
Exception childException = Mockito.mock(Exception.class);
Mockito.when(childException.getCause()).thenReturn(null);
Mockito.when(childException.toString()).thenReturn("child exception toString()");
Exception parentException = Mockito.mock(Exception.class);
Mockito.when(parentException.getCause()).thenReturn(childException);
Exception grandparentException = Mockito.mock(Exception.class);
Mockito.when(grandparentException.getCause()).thenReturn(parentException);

// prepare analytics so we can inspect what happens
ExceptionHit spyHit = Mockito.spy(new ExceptionHit());
Mockito.doReturn(spyHit).when(mAnalytics).exception();

try {
UsageAnalytics.sendAnalyticsException(grandparentException, false);
} catch (Exception e) {
// do nothing - this is expected because UsageAnalytics isn't fully initialized
try (
MockedStatic<PreferenceManager> ignored = mockStatic(PreferenceManager.class);
MockedStatic<GoogleAnalytics> ignored1 = mockStatic(GoogleAnalytics.class)) {

when(PreferenceManager.getDefaultSharedPreferences(ArgumentMatchers.any()))
.thenReturn(mMockSharedPreferences);

when(GoogleAnalytics.builder())
.thenReturn(new SpyGoogleAnalyticsBuilder());

// This is actually a Mockito Spy of GoogleAnalyticsImpl
GoogleAnalytics mAnalytics = UsageAnalytics.initialize(mMockContext);

// no root cause
Exception exception = mock(Exception.class);
when(exception.getCause()).thenReturn(null);
Throwable cause = UsageAnalytics.getCause(exception);
verify(exception).getCause();
Assert.assertEquals(exception, cause);

// a 3-exception chain inside the actual analytics call
Exception childException = mock(Exception.class);
when(childException.getCause()).thenReturn(null);
when(childException.toString()).thenReturn("child exception toString()");
Exception parentException = mock(Exception.class);
when(parentException.getCause()).thenReturn(childException);
Exception grandparentException = mock(Exception.class);
when(grandparentException.getCause()).thenReturn(parentException);

// prepare analytics so we can inspect what happens
ExceptionHit spyHit = spy(new ExceptionHit());
doReturn(spyHit).when(mAnalytics).exception();

try {
UsageAnalytics.sendAnalyticsException(grandparentException, false);
} catch (Exception e) {
// do nothing - this is expected because UsageAnalytics isn't fully initialized
}
verify(grandparentException).getCause();
verify(parentException).getCause();
verify(childException).getCause();
verify(mAnalytics).exception();
verify(spyHit).exceptionDescription(ArgumentMatchers.anyString());
verify(spyHit).sendAsync();
Assert.assertEquals(spyHit.exceptionDescription(), "child exception toString()");
}
Mockito.verify(grandparentException).getCause();
Mockito.verify(parentException).getCause();
Mockito.verify(childException).getCause();
Mockito.verify(mAnalytics).exception();
Mockito.verify(spyHit).exceptionDescription(ArgumentMatchers.anyString());
Mockito.verify(spyHit).sendAsync();
Assert.assertEquals(spyHit.exceptionDescription(), "child exception toString()");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,35 +18,34 @@

import android.util.Log;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.mockito.MockedStatic;

import timber.log.Timber;

import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeast;
import static org.powermock.api.mockito.PowerMockito.verifyStatic;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;

@RunWith(PowerMockRunner.class)
@PrepareForTest(Log.class)
public class ProductionCrashReportingTreeTest {


@Before
public void setUp() {

// setup - simply instrument the class and do same log init as production
PowerMockito.mockStatic(Log.class);
Timber.plant(new AnkiDroidApp.ProductionCrashReportingTree());
}

@After
public void tearDown() {
Timber.uprootAll();
}


/**
* The Production logger ignores verbose and debug logs on purpose
Expand All @@ -55,20 +54,30 @@ public void setUp() {
@Test
public void testProductionDebugVerboseIgnored() {

// set up the platform log so that if anyone calls these 2 methods at all, it throws
Mockito.when(Log.v(anyString(), anyString(), any()))
.thenThrow(new RuntimeException("Verbose logging should have been ignored"));
Mockito.when(Log.d(anyString(), anyString(), any()))
.thenThrow(new RuntimeException("Debug logging should be ignored"));

// now call our wrapper - if it hits the platform logger it will throw
try {
Timber.v("verbose");
Timber.d("debug");
} catch (Exception e) {
Assert.fail("we were unable to log without exception?");
try (MockedStatic<Log> ignored = mockStatic(Log.class)) {
// set up the platform log so that if anyone calls these 2 methods at all, it throws
when(Log.v(anyString(), anyString(), any()))
.thenThrow(new RuntimeException("Verbose logging should have been ignored"));
when(Log.d(anyString(), anyString(), any()))
.thenThrow(new RuntimeException("Debug logging should be ignored"));
when(Log.i(anyString(), anyString(), any()))
.thenThrow(new RuntimeException("Info logging should throw!"));

// now call our wrapper - if it hits the platform logger it will throw
try {
Timber.v("verbose");
Timber.d("debug");
} catch (Exception e) {
Assert.fail("we were unable to log without exception?");
}

try {
Timber.i("info");
Assert.fail("we should have gone to Log.i and thrown but did not? Testing mechanism failure.");
} catch (Exception e) {
// this means everything worked, we were counting on an exception
}
}

}


Expand All @@ -82,19 +91,21 @@ public void testProductionDebugVerboseIgnored() {
@SuppressWarnings("PMD.JUnitTestsShoudIncludAssert")
public void testProductionLogTag() {

// setUp() instrumented the static, now exercise it
Timber.i("info level message");
Timber.w("warn level message");
Timber.e("error level message");

// verify that info level had the constant tag
verifyStatic(Log.class, atLeast(1));
Log.i(AnkiDroidApp.TAG, "info level message", null);

// verify Warn/Error has final part of calling class name to start the message
verifyStatic(Log.class, atLeast(1));
Log.w(AnkiDroidApp.TAG, this.getClass().getSimpleName() + "/ " + "warn level message", null);
verifyStatic(Log.class, atLeast(1));
Log.e(AnkiDroidApp.TAG, this.getClass().getSimpleName() + "/ " + "error level message", null);
try (MockedStatic<Log> autoClosed = mockStatic(Log.class)) {

// Now let's run through our API calls...
Timber.i("info level message");
Timber.w("warn level message");
Timber.e("error level message");

// ...and make sure they hit the logger class post-processed correctly
try {
autoClosed.verify(() -> Log.i(AnkiDroidApp.TAG, "info level message", null));
autoClosed.verify(() -> Log.w(AnkiDroidApp.TAG, this.getClass().getSimpleName() + "/ " + "warn level message", null));
autoClosed.verify(() -> Log.e(AnkiDroidApp.TAG, this.getClass().getSimpleName() + "/ " + "error level message", null));
} catch (Exception e) {
Assert.fail("Mockito verification failed unexpectedly");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.powermock.modules.junit4.PowerMockRunner;
import org.mockito.MockitoAnnotations;

import static com.ichi2.testutils.AnkiAssert.assertDoesNotThrow;
import static com.ichi2.utils.FunctionalInterfaces.Supplier;
Expand All @@ -16,8 +15,6 @@
import static org.mockito.ArgumentMatchers.eq;

//Unknown issue: @CheckResult should provide warnings on this class when return value is unused, but doesn't.
//TODO: The preference mock is messy
@RunWith(PowerMockRunner.class)
public class PreferenceExtensionsTest {

private static Supplier<String> UNUSED_SUPPLIER = () -> { throw new UnexpectedException();};
Expand All @@ -40,6 +37,7 @@ private String getOrSetString(String key, Supplier<String> supplier) {

@Before
public void setUp() {
MockitoAnnotations.openMocks(this);
Mockito.when(mMockReferences.contains(VALID_KEY)).thenReturn(true);
Mockito.when(mMockReferences.getString(eq(VALID_KEY), anyString())).thenReturn(VALID_RESULT);
Mockito.when(mMockReferences.edit()).thenReturn(mockEditor);
Expand Down

0 comments on commit bb742f2

Please sign in to comment.