PowerMock + Robolectric + Dagger2. Part I

2019-04-11 08:32发布

问题:


This question was created from first part of PowerMock + Robolectric + Dagger2
So i'm a little bit again. Sorry.

I test custom view class which contain:

  1. android ui elements
  2. some logic
  3. static methods callings
  4. dagger2 dependencies


So i use next tools for testing

  1. Robolectric for UI elements mocking
  2. unit tests for logic testing
  3. PowerMock for static methods mocking

Robolectric + PowerMock integration problem is known and solution is known - https://github.com/robolectric/robolectric/wiki/Using-PowerMock
But with this solution dagger2 dependencies fail.

Attention to code
My custom view - ProgressTextView:

public class ProgressTextView extends TextView {

    private String defaultText;
    private int fileSize;
    private String fileSizeString;
    private FileDownloaderI fileDownloader;

    @Inject
    FileDownloaderManager fileDownloaderManager;

    Subscription downloadProgressChannelSubscription;
    Subscription downloadCancelChannelSubscription;

    public ProgressTextView(Context context) {
        super(context);
        provideDependency();
    }

    public ProgressTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        provideDependency();
    }

    public ProgressTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        provideDependency();
    }

    private void provideDependency() {
        ApplicationSIP.get().applicationComponent().inject(this);
    }

}

ApplicationSIP:

public class ApplicationSIP extends Application {

    public static volatile Context applicationContext;

    @NonNull
    private AppComponent appComponent;

    @Override
    public void onCreate() {
        super.onCreate();
        applicationContext = getApplicationContext();
        Logger.registerLogger(this);

        appComponent = prepareApplicationComponent().build();
        appComponent.inject(this);
    }

    @NonNull
    protected DaggerAppComponent.Builder prepareApplicationComponent() {
        return DaggerAppComponent.builder()
                .appModule(new AppModule(getApplicationContext()))
                .tGModule(new TGModule());
    }

    @NonNull
    public AppComponent applicationComponent() {
        return appComponent;
    }

    @NonNull
    public static ApplicationSIP get() {
        return (ApplicationSIP) applicationContext.getApplicationContext();
    }

}

AppComponent:

@Component(modules = {AppModule.class, TGModule.class, MediaModule.class, StaticModule.class})
@Singleton
public interface AppComponent {   
    void inject(ApplicationSIP applicationSIP);
    void inject(ProgressDownloadView progressDownloadView);
    void inject(ProgressTextView progressTextView);
}

And ProgressTextViewTest:

@RunWith(RobolectricUnitTestRunner.class)
@PowerMockIgnore({ "org.mockito.*", "org.robolectric.*", "android.*" })
@PrepareForTest(Formatter.class)
public class ProgressTextViewTest {

    @Rule
    public PowerMockRule rule = new PowerMockRule();
    Activity activity;

    @Before
    public void beforeTest() {
        // PowerMockito
        PowerMockito.mockStatic(Formatter.class);
        Mockito.when(Formatter.formatFileSize(anyObject(), anyInt())).thenReturn("");
        // Robolectic
        activity = Robolectric.setupActivity(Activity.class);
    }

    @Test
    public void init_FileDownloaded() {
        ProgressTextView progressTextView = new ProgressTextView(activity);
        ...
    }
}

So when i start this test i get next exception:

java.lang.NullPointerException
at com.tg.osip.ApplicationSIP.get(ApplicationSIP.java:64)
at com.tg.osip.ui.general.views.ProgressTextView.provideDependency(ProgressTextView.java:60)
at com.tg.osip.ui.general.views.ProgressTextView.<init>(ProgressTextView.java:46)
at com.tg.osip.ui.general.views.ProgressTextViewTest.init_FileDownloaded(ProgressTextViewTest.java:67)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
at org.powermock.modules.junit4.rule.PowerMockStatement$1.run(PowerMockRule.java:52)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at org.powermock.reflect.internal.WhiteboxImpl.performMethodInvocation(WhiteboxImpl.java:1873)
at org.powermock.reflect.internal.WhiteboxImpl.doInvokeMethod(WhiteboxImpl.java:773)
at org.powermock.reflect.internal.WhiteboxImpl.invokeMethod(WhiteboxImpl.java:638)
at org.powermock.reflect.Whitebox.invokeMethod(Whitebox.java:401)
at org.powermock.classloading.ClassloaderExecutor.execute(ClassloaderExecutor.java:98)
at org.powermock.classloading.ClassloaderExecutor.execute(ClassloaderExecutor.java:78)
at org.powermock.modules.junit4.rule.PowerMockStatement.evaluate(PowerMockRule.java:49)
at org.robolectric.RobolectricTestRunner$2.evaluate(RobolectricTestRunner.java:251)
at org.robolectric.RobolectricTestRunner.runChild(RobolectricTestRunner.java:188)
at org.robolectric.RobolectricTestRunner.runChild(RobolectricTestRunner.java:54)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.robolectric.RobolectricTestRunner$1.evaluate(RobolectricTestRunner.java:152)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:78)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:212)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:68)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:140)

If you change robolectric activity init placing:

@RunWith(RobolectricUnitTestRunner.class)
@PowerMockIgnore({ "org.mockito.*", "org.robolectric.*", "android.*" })
@PrepareForTest(Formatter.class)
public class ProgressTextViewTest {

    @Rule
    public PowerMockRule rule = new PowerMockRule();
    Activity activity = Robolectric.setupActivity(Activity.class);

    @Before
    public void beforeTest() {
        // PowerMockito
        PowerMockito.mockStatic(Formatter.class);
        Mockito.when(Formatter.formatFileSize(anyObject(), anyInt())).thenReturn("");
    }

    @Test
    public void init_FileDownloaded() {
        ProgressTextView progressTextView = new ProgressTextView(activity);
        ...
    }
}

I get other exception:

java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:137)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:121)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:569)
at java.lang.StringBuffer.append(StringBuffer.java:369)
at java.io.StringWriter.write(StringWriter.java:94)
at com.thoughtworks.xstream.core.util.QuickWriter.flush(QuickWriter.java:73)
at com.thoughtworks.xstream.io.xml.PrettyPrintWriter.flush(PrettyPrintWriter.java:342)
at com.thoughtworks.xstream.XStream.toXML(XStream.java:858)
at com.thoughtworks.xstream.XStream.toXML(XStream.java:843)
at org.powermock.classloading.DeepCloner.clone(DeepCloner.java:53)
at org.powermock.classloading.ClassloaderExecutor.execute(ClassloaderExecutor.java:89)
at org.powermock.classloading.ClassloaderExecutor.execute(ClassloaderExecutor.java:78)
at org.powermock.modules.junit4.rule.PowerMockStatement.evaluate(PowerMockRule.java:49)
at org.robolectric.RobolectricTestRunner$2.evaluate(RobolectricTestRunner.java:251)
at org.robolectric.RobolectricTestRunner.runChild(RobolectricTestRunner.java:188)
at org.robolectric.RobolectricTestRunner.runChild(RobolectricTestRunner.java:54)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.robolectric.RobolectricTestRunner$1.evaluate(RobolectricTestRunner.java:152)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:68)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

回答1:

Before answering your question. I would say:

  1. Your view is doing/knowing too much. I'm talking about knowledge of FileDownloaderManager and knowledge about how to get dependencies
  2. Since you're using injection I would wrap static functions into a class and inject it. It will be much easier to write/maintain tests
  3. Even name of class AppComponent says that it should not know about how to inject a view. Think about scoped injections
  4. Some minors with applicationContext. Why it is volatile, do you expect it to be accessed from non-main thread? It is already ApplicationSIP why do you need play with application context and casting?

Finally to the answer. The first stack trace says that the appContext is null. So onCreate of your app was not called. I don't see the code of RobolectricUnitTestRunner but I assume the problem is in the usage of the PowerMock. The second stack trace kind of proof for that. So I would say the answer is to stop usage of the PowerMock.



回答2:

Summary
Don't use PowerMock with Robolectric and Dagger2. It's not safe.
Now i use wrap classes for static methods and inject this classes with Dagger2 helping. So there are not static methods in my testing class and PowerMock does not need.
I think that it is quite normal variant.
And thanks Eugen Martynov for helping.