Network-dependent Android screens are hard to test when the UI waits for a live backend that can be slow, unavailable, or filled with changing data. A local MockWebServer fixture lets an Espresso test feed one known HTTP response to the app and assert the rendered screen state that response should produce.
MockWebServer runs inside the instrumented test process and exposes an HTTP base URL that the app can use during the test. The app still exercises its real HTTP client, JSON parser, repository, and view code, but the backend response comes from the queued fixture instead of a remote service.
Install the network override before launching the activity so cached clients do not keep the production URL. Keep the override and any cleartext permission in debug or androidTest wiring, and reset both the app hook and server after each test.
Related: How to write an Espresso login flow test
Related: How to add an Espresso IdlingResource
Related: How to run Espresso tests locally
android { defaultConfig { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } } val espressoVersion = "3.7.0" val androidxTestVersion = "1.7.0" val junitVersion = "1.3.0" val mockWebServerVersion = "5.4.0" dependencies { androidTestImplementation( "androidx.test.espresso:" + "espresso-core:$espressoVersion" ) androidTestImplementation( "androidx.test:core:$androidxTestVersion" ) androidTestImplementation( "androidx.test:runner:$androidxTestVersion" ) androidTestImplementation( "androidx.test.ext:junit:$junitVersion" ) androidTestImplementation( "com.squareup.okhttp3:" + "mockwebserver:$mockWebServerVersion" ) }
The versions shown match the current AndroidX Test releases in Google Maven and the current MockWebServer release in Maven Central. Use the app's version catalog or dependency convention when the project already centralizes test libraries.
package com.example.orders.net import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl object NetworkConfig { private val productionBaseUrl = "https://api.example.com/".toHttpUrl() @Volatile var testBaseUrl: HttpUrl? = null val ordersBaseUrl: HttpUrl get() = testBaseUrl ?: productionBaseUrl fun resetForTests() { testBaseUrl = null } }
Use the app's existing dependency injection or service locator when it already owns API construction. The important boundary is that the test can set the base URL before the activity, repository, or Retrofit service is created.
package com.example.orders.net import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory object OrderApiFactory { fun create(): OrderApi { return Retrofit.Builder() .baseUrl(NetworkConfig.ordersBaseUrl) .addConverterFactory(MoshiConverterFactory.create()) .build() .create(OrderApi::class.java) } }
Do not let an androidTest run keep a production API singleton that was created before the override. Reset cached repositories or recreate the dependency graph before launching the screen under test.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <application android:usesCleartextTraffic="true" /> </manifest>
Keep cleartext permission in debug or test-only configuration. Do not loosen the release app network policy only to make a mock server test pass.
package com.example.orders import androidx.test.core.app.ActivityScenario import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import com.example.orders.net.NetworkConfig import java.util.concurrent.TimeUnit import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import org.junit.After import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) @LargeTest class OrderNetworkMockTest { private lateinit var server: MockWebServer @Before fun startServer() { server = MockWebServer() server.start() NetworkConfig.testBaseUrl = server.url("/") AppGraph.resetForTests() } @After fun stopServer() { NetworkConfig.resetForTests() AppGraph.resetForTests() server.shutdown() } @Test fun openOrdersShowsMockedOrder() { server.enqueue( MockResponse() .setResponseCode(200) .setHeader("Content-Type", "application/json") .setBody( """ { "orders": [ { "id": "A-104", "title": "Coffee beans", "status": "Ready" } ] } """.trimIndent() ) ) val scenario = ActivityScenario.launch(OrderActivity::class.java) try { onView(withId(R.id.loadOrdersButton)).perform(click()) onView(withId(R.id.orderTitle)) .check(matches(withText("Coffee beans"))) onView(withId(R.id.orderStatus)) .check(matches(withText("Ready"))) } finally { scenario.close() } val request = server.takeRequest(2, TimeUnit.SECONDS) assertEquals("/orders", request?.path) } }
MockWebServer controls the response data, not asynchronous timing. Register the app's IdlingResource or another Espresso-aware wait before this assertion when the screen updates after background work.
Related: How to add an Espresso IdlingResource
The UI assertion should prove that the mocked response reached the screen. The recorded-request assertion should prove that the app called the expected test server path instead of a live backend.
$ ./gradlew :app:connectedDebugAndroidTest Task :app:connectedDebugAndroidTest Starting 1 tests on Pixel_8_API_35 OrderNetworkMockTest > openOrdersShowsMockedOrder PASSED BUILD SUCCESSFUL in 38s
The task name changes with the module and variant. Android projects with product flavors may expose a task such as :app:connectedFreeDebugAndroidTest.