square / okhttp

Square’s meticulous HTTP client for the JVM, Android, and GraalVM.
https://square.github.io/okhttp/
Apache License 2.0
45.87k stars 9.16k forks source link

NetworkOnMainThreadException #3184

Closed rusmichal closed 7 years ago

rusmichal commented 7 years ago

Hey. I know that there already is lot of similar issues but I cannot find solution. I've started working with Espresso UI tests. I prepare custom MockTestRunner, MockApplication for initialization Dagger components and I've defined mock modules too. It looks like that:

public class MockTestRunner extends AndroidJUnitRunner {
        public Application newApplication(ClassLoader cl, String className, Context context)
                throws InstantiationException, IllegalAccessException, ClassNotFoundException {
            return super.newApplication(cl, MockMyApplication.class.getName(), context);
        }
 }

MyApp is extended by

public class MockQrApplication extends MyApp {
    private MockWebServer mockWebServer;

    protected void initComponent() {
        mockWebServer = new MockWebServer();

        component = DaggerMyAppComponent
                .builder()
                .myAppModule(new MyAppModule(this))
                .busModule(new BusModule())
                .apiModule(new MockApiModule(mockWebServer))
                .facebookModule(new FacebookModule())
                .dataManagerModule(new DataManagerModule())
                .greenDaoModule(new GreenDaoModule())
                .trackModule(new TrackModule(this))
                .build();

        component.inject(this);
    }
}

I added testInstrumentationRunner into gradle

defaultConfig {
        ....
        multiDexEnabled true

        testInstrumentationRunner "a.b.c.MockTestRunner"
    }

I want run login tests in my LoginActivity

@RunWith(AndroidJUnit4.class)
@LargeTest
public class LoginActivityTest {
    protected Solo solo;

    @Rule
    public ActivityTestRule<LoginActivity> activityTestRule = new ActivityTestRule(LoginActivity.class);

    @Before
    public void setUp() throws Exception {
        initVariables();
    }

    protected void initVariables() {
        solo = new Solo(InstrumentationRegistry.getInstrumentation(), activityTestRule.getActivity());
    }

    @Test
    public void testLayout() {
        solo.waitForFragmentByTag(LoginFragment.TAG, 1000);

        onView(withId(R.id.email_input)).perform(clearText(), typeText("developer@appppp.com"));
        onView(withId(R.id.pass_input)).perform(clearText(), typeText("qqqqqqqq"));
        onView(withId(R.id.login_button)).perform(click());

        solo.waitForDialogToOpen();
    }
}

This is MockApiModule which extends ApiModule class

public class MockApiModule extends ApiModule {
    private MockWebServer mockWebServer;

    public MockApiModule(MockWebServer mockWebServer) {
        this.mockWebServer = mockWebServer;
    }

    @Override
    public OkHttpClient provideOkHttpClient(DataManager dataManager) {
        return new OkHttpClient.Builder()
                .build();
    }

    @Override
    public Retrofit provideRetrofit(OkHttpClient okHttpClient) {
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(mockWebServer.url("/"))       // throw NetworkOnMainThreadException
                .addConverterFactory(NullOnEmptyConverterFactory.create())
                .addConverterFactory(GsonConverterFactory.create(new Gson()))
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .client(okHttpClient)
                .build();
        return retrofit;
    }

    @Override
    public ApiService provideApiService(Retrofit retrofit) {
        return retrofit.create(ApiService.class);
    }

    @Override
    public ApiClient provideApiManager(Application application, ApiService apiService, DataManager dataManager) {
        return new MockApiClient(application, apiService, dataManager, mockWebServer);
    }
}

API login request looks like that:

public void login(UserLoginModel userLoginModel, final Account.Login callback) {
        final CountDownLatch latch = new CountDownLatch(1);

        mockWebServer.enqueue(MockResponse.getMockResponse(200, MockResponse.getResourceAsString(this, "login.json")));

        super.login(userLoginModel, new Account.Login() {
            @Override
            public void onLoginSuccess(LoginResponse response) {
                callback.onLoginSuccess(response);

                latch.countDown();
            }

            @Override
            public void onLoginFail(String message) {

            }
        });

        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

It works if I change MockMyApplication into MyApp class of application in MockTestRunner

When I want to run my tests I got:

android.os.NetworkOnMainThreadException
at android.os.StrictMode$AndroidBlockGuardPolicy.onNetwork(StrictMode.java:1285)
at java.net.InetAddress.lookupHostByName(InetAddress.java:431)
at java.net.InetAddress.getAllByNameImpl(InetAddress.java:252)
at java.net.InetAddress.getByName(InetAddress.java:305)
at okhttp3.mockwebserver.MockWebServer.start(MockWebServer.java:303)
at okhttp3.mockwebserver.MockWebServer.start(MockWebServer.java:293)
at okhttp3.mockwebserver.MockWebServer.maybeStart(MockWebServer.java:143)
at okhttp3.mockwebserver.MockWebServer.getHostName(MockWebServer.java:172)
at okhttp3.mockwebserver.MockWebServer.url(MockWebServer.java:198)
at com.mooduplabs.qrcontacts.modules.MockApiModule.provideRetrofit(MockApiModule.java:38)
at com.mooduplabs.qrcontacts.modules.ApiModule_ProvideRetrofitFactory.get(ApiModule_ProvideRetrofitFactory.java:23)
at com.mooduplabs.qrcontacts.modules.ApiModule_ProvideRetrofitFactory.get(ApiModule_ProvideRetrofitFactory.java:9)
at dagger.internal.ScopedProvider.get(ScopedProvider.java:46)
at com.mooduplabs.qrcontacts.modules.ApiModule_ProvideApiServiceFactory.get(ApiModule_ProvideApiServiceFactory.java:23)
at com.mooduplabs.qrcontacts.modules.ApiModule_ProvideApiServiceFactory.get(ApiModule_ProvideApiServiceFactory.java:9)
at dagger.internal.ScopedProvider.get(ScopedProvider.java:46)
at com.mooduplabs.qrcontacts.modules.ApiModule_ProvideApiManagerFactory.get(ApiModule_ProvideApiManagerFactory.java:31)
at com.mooduplabs.qrcontacts.modules.ApiModule_ProvideApiManagerFactory.get(ApiModule_ProvideApiManagerFactory.java:11)
at dagger.internal.ScopedProvider.get(ScopedProvider.java:46)
at com.mooduplabs.qrcontacts.activities.BaseActivity_MembersInjector.injectMembers(BaseActivity_MembersInjector.java:44)
at com.mooduplabs.qrcontacts.activities.BaseActivity_MembersInjector.injectMembers(BaseActivity_MembersInjector.java:13)
at com.mooduplabs.qrcontacts.components.DaggerQrContactsAppComponent.inject(DaggerQrContactsAppComponent.java:91)
at com.mooduplabs.qrcontacts.activities.BaseActivity.init(BaseActivity.java:74)
at com.mooduplabs.qrcontacts.activities.BaseActivity.onCreate(BaseActivity.java:64)
at android.app.Activity.performCreate(Activity.java:6367)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1110)
at android.support.test.runner.MonitoringInstrumentation.callActivityOnCreate(MonitoringInstrumentation.java:532)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2404)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2511)
at android.app.ActivityThread.access$900(ActivityThread.java:165)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1375)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:150)
at android.app.ActivityThread.main(ActivityThread.java:5621)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:794)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:684)
swankjesse commented 7 years ago

You’ll need to avoid starting your MockWebServer on the main thread.

rusmichal commented 7 years ago

But how?

DavidEdwards commented 7 years ago

@rusmichal

If you are confused about threading, you should take a look at this:

http://stackoverflow.com/questions/6343166/how-to-fix-android-os-networkonmainthreadexception

There are some good explanations, and some solutions.

rusmichal commented 7 years ago

I guess you don't understand me. I know that about NetworkOnMainThreadException and why it is appearing in Android OS. But in my test in line .baseUrl(mockWebServer.url("/")) throw NetworkOnMainThreadException. I don't understand why in this line?

Maybe I will explain more what I want get. I have finished my app, couple of activties and fragments. I have class BasicFragment where I inject ApiClient object to request with backend. For example let get LoginFragment. There is simple form fields email and password and login button. In my UI tests I type email and password and all looks like real user types. I added performClick for login button. So It start calls real backend. This I want change to call mock server and emit mock response from json file.

This is weird because I have Unit test where I test backend stuff and I use MockWebserver and there it works fine.

My first post is instrumental test but below is my unit test and it works:

public class TestApiModule extends ApiModule {
    private Context context;
    private DataManager dataManager;
    private String baseUrl;

    public TestApiModule(String baseUrl, Context context, DataManager dataManager) {
        this.baseUrl = baseUrl;
        this.context = context;
        this.dataManager = dataManager;
    }

    @Override
    public OkHttpClient provideOkHttpClient(DataManager dataManager) {
        return new OkHttpClient.Builder()
                .build();
    }

    @Override
    public Retrofit provideRetrofit(OkHttpClient okHttpClient) {
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(baseUrl)
                .addConverterFactory(GsonConverterFactory.create(new Gson()))
                .client(okHttpClient)
                .build();
        return retrofit;
    }

    @Override
    public ApiService provideApiService(Retrofit retrofit) {
        return retrofit.create(ApiService.class);
    }

    public ApiClient provideApiManager() {
        OkHttpClient okHttpClient = provideOkHttpClient(dataManager);
        Retrofit retrofit = provideRetrofit(okHttpClient);
        ApiService apiService = provideApiService(retrofit);

        return new ApiClient(context, apiService, dataManager);
    }
}
public class BaseApiModuleTest {

    @Mock
    protected Context context;

    protected DataManager dataManager;
    protected MockWebServer mockWebServer;
    protected ApiClient apiClient;

    @Before
    public void setup() throws Exception {
        MockitoAnnotations.initMocks(this);
        mockWebServer = new MockWebServer();
        dataManager = new DataManager(context);
        apiClient = new TestApiModule(
                mockWebServer.url("/").toString(),
                context,
                dataManager
        ).provideApiManager();
    }

    @Test
    public void testSetupIsCorrect() throws Exception {
        assertNotNull(dataManager);
        assertNotNull(context);
        assertNotNull(mockWebServer);
        assertNotNull(apiClient);
    }

}
DavidEdwards commented 7 years ago
public class MockQrApplication extends MyApp {
    private MockWebServer mockWebServer;

    protected void initComponent() {
        mockWebServer = new MockWebServer();

        component = DaggerMyAppComponent
                .builder()
                .myAppModule(new MyAppModule(this))
                .busModule(new BusModule())
                .apiModule(new MockApiModule(mockWebServer))
                .facebookModule(new FacebookModule())
                .dataManagerModule(new DataManagerModule())
                .greenDaoModule(new GreenDaoModule())
                .trackModule(new TrackModule(this))
                .build();

        component.inject(this);
    }
}

I am not very familiar with Dagger. However, to me it looks like this method is creating MockWebServerand Dagger is starting the mockWebServer? If that is the case, you should check whether this thread is running on the Main Thread.

You can output to logcat to determine if this is where the issue is. In these two places add the following code.

Log.w("MAIN_THREAD_TEST", "On main thread="+Looper.getMainLooper().equals(Looper.myLooper()));

rusmichal commented 7 years ago

Class MyApp extends MultidexApplication where I initialize all components needed in app (Dagger). MockQrApplication extends MyApp and override initComponent for initialization mock modules for tests. So I don't have to change my production code only I provide mock modules.

Both logs are:

W/MAIN_THREAD_TEST: On main thread=true
W/MAIN_THREAD_TEST: On main thread=true
DavidEdwards commented 7 years ago

Since both logs output true, it seems clear to me that you are creating your mockWebServer on the main thread. So my first step would be to push that onto a background thread.

I am not familiar with the lifecycles of the classes you are using. However, you could attempt to initialize / start the mockWebServer in its own thread.

One example:

new Thread(new Runnable() {
    @Override
    public void run() {
        mockWebServer = new MockWebServer();
        mockWebServer.start();
    }
});
rusmichal commented 7 years ago

It doesn't work because mockWebServer is null in MainThread.

I tried this

public class MockQrApplication extends QrContacts {
    private MockWebServer mockWebServer;

    protected void initComponent() {

        MyThread thread = new MyThread();
        thread.start();
        mockWebServer = thread.mockWebServerInThread;

        component = DaggerQrContactsAppComponent
                .builder()
                .qrContactsAppModule(new QrContactsAppModule(this))
                .busModule(new BusModule())
                .apiModule(new MockApiModule(mockWebServer))
                .facebookModule(new FacebookModule())
                .dataManagerModule(new DataManagerModule())
                .greenDaoModule(new GreenDaoModule())
                .trackModule(new TrackModule(this))
                .build();

        component.inject(this);
    }

    class MyThread extends Thread {

        public MockWebServer mockWebServerInThread;

        @Override
        public void run() {
            mockWebServerInThread = new MockWebServer();
            try {
                mockWebServerInThread.start();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }
}

In your solution and my (above) I got this:

java.lang.NullPointerException: Attempt to invoke virtual method 'okhttp3.HttpUrl okhttp3.mockwebserver.MockWebServer.url(java.lang.String)' on a null object reference
at com.mooduplabs.qrcontacts.modules.MockApiModule.provideRetrofit(MockApiModule.java:38)
at com.mooduplabs.qrcontacts.modules.ApiModule_ProvideRetrofitFactory.get(ApiModule_ProvideRetrofitFactory.java:23)
at com.mooduplabs.qrcontacts.modules.ApiModule_ProvideRetrofitFactory.get(ApiModule_ProvideRetrofitFactory.java:9)
at dagger.internal.ScopedProvider.get(ScopedProvider.java:46)
at com.mooduplabs.qrcontacts.modules.ApiModule_ProvideApiServiceFactory.get(ApiModule_ProvideApiServiceFactory.java:23)
at com.mooduplabs.qrcontacts.modules.ApiModule_ProvideApiServiceFactory.get(ApiModule_ProvideApiServiceFactory.java:9)
at dagger.internal.ScopedProvider.get(ScopedProvider.java:46)
at com.mooduplabs.qrcontacts.modules.ApiModule_ProvideApiManagerFactory.get(ApiModule_ProvideApiManagerFactory.java:31)
at com.mooduplabs.qrcontacts.modules.ApiModule_ProvideApiManagerFactory.get(ApiModule_ProvideApiManagerFactory.java:11)
at dagger.internal.ScopedProvider.get(ScopedProvider.java:46)
at com.mooduplabs.qrcontacts.activities.BaseActivity_MembersInjector.injectMembers(BaseActivity_MembersInjector.java:44)
at com.mooduplabs.qrcontacts.activities.BaseActivity_MembersInjector.injectMembers(BaseActivity_MembersInjector.java:13)
at com.mooduplabs.qrcontacts.components.DaggerQrContactsAppComponent.inject(DaggerQrContactsAppComponent.java:91)
at com.mooduplabs.qrcontacts.activities.BaseActivity.init(BaseActivity.java:74)
at com.mooduplabs.qrcontacts.activities.BaseActivity.onCreate(BaseActivity.java:64)
at android.app.Activity.performCreate(Activity.java:6367)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1110)
at android.support.test.runner.MonitoringInstrumentation.callActivityOnCreate(MonitoringInstrumentation.java:532)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2404)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2511)
at android.app.ActivityThread.access$900(ActivityThread.java:165)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1375)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:150)
at android.app.ActivityThread.main(ActivityThread.java:5621)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:794)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:684)
swankjesse commented 7 years ago

No action for us to take on this.

RafalManka commented 5 years ago

If anyone else stumbles on this problem here is the solution. Modify your custom AndroidJUnitRunner like so:

class EspressoRunner : AndroidJUnitRunner() {

    override fun onCreate(arguments: Bundle) {
        StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder().permitAll().build())
        super.onCreate(arguments)
    }

    ...

}

copied from this article.