network-tests

$npx mdskill add stripe/stripe-android/network-tests

For instrumentation tests that need mocked network responses, use `NetworkRule` (from `network-testing` module) with `testBodyFromFile`. JSON fixture files live in the module's `src/androidTest/resources/` directory (e.g., `paymentsheet/src/androidTest/resources/checkout-session-init.json`) and are resolved by filename from the resources root.

SKILL.md

.github/skills/network-testsView on GitHub ↗
---
name: network-tests
description: Use when writing NetworkRule integration tests in stripe-android — covers testBodyFromFile, inline JSON modification, request matchers, and fixture patterns
---
# NetworkRule Integration Tests

For instrumentation tests that need mocked network responses, use `NetworkRule` (from `network-testing` module) with `testBodyFromFile`. JSON fixture files live in the module's `src/androidTest/resources/` directory (e.g., `paymentsheet/src/androidTest/resources/checkout-session-init.json`) and are resolved by filename from the resources root.

## Basic Structure

```kotlin
@RunWith(AndroidJUnit4::class)
internal class MyFeatureTest {
    @get:Rule
    val testRules: TestRules = TestRules.create()

    private val networkRule = testRules.networkRule

    @Test
    fun testSomething() = runPaymentSheetTest(
        networkRule = networkRule,
        resultCallback = ::assertCompleted,
    ) { testContext ->
        networkRule.enqueue(requestMatcher) { response ->
            response.testBodyFromFile("my-fixture.json")
        }
        // ... test actions ...
    }
}
```

## Prefer Inline JSON Modification Over New Fixture Files

When a test needs a modified JSON response, use the `testBodyFromFile` lambda to modify the base fixture inline — do NOT create a separate JSON file for each variation.

```kotlin
// GOOD: Modify the base fixture inline
networkRule.checkoutInit { response ->
    response.testBodyFromFile("checkout-session-init.json") { json ->
        json.put("customer_email", "[email protected]")
    }
}

// BAD: Creating checkout-session-init-with-email.json with one field different
networkRule.checkoutInit { response ->
    response.testBodyFromFile("checkout-session-init-with-email.json")
}
```

This keeps the fixture set minimal and makes the test-specific modifications explicit at the call site.

## Composing Multiple Modifications

The lambda receives a `JSONObject` — use standard `org.json` methods to add or modify fields:

```kotlin
networkRule.checkoutInit { response ->
    response.testBodyFromFile("checkout-session-init.json") { json ->
        json.put("customer", JSONObject("""
            {
                "id": "cus_12345",
                "payment_methods": [],
                "can_detach_payment_method": true
            }
        """.trimIndent()))
        json.put("customer_managed_saved_payment_methods_offer_save", JSONObject("""
            {"enabled": true, "status": "not_accepted"}
        """.trimIndent()))
    }
}
```

For nested modifications, chain `getJSONObject()`:

```kotlin
response.testBodyFromFile("checkout-session-init.json") { json ->
    json.getJSONObject("server_built_elements_session_params")
        .getJSONObject("deferred_intent")
        .put("setup_future_usage", "off_session")
}
```

## Extracting Shared Modifiers

When multiple tests share the same JSON modification, extract the lambda as a parameter:

```kotlin
private fun runMyTest(
    jsonModifier: (JSONObject) -> Unit = {},
) = runPaymentSheetTest(networkRule = networkRule, resultCallback = ::assertCompleted) { testContext ->
    networkRule.checkoutInit { response ->
        response.testBodyFromFile("checkout-session-init.json", jsonModifier)
    }
    // ... shared test logic ...
}

@Test
fun testWithSfu() = runMyTest { json ->
    json.getJSONObject("server_built_elements_session_params")
        .getJSONObject("deferred_intent")
        .put("setup_future_usage", "off_session")
}
```

## testBodyFromFile Variants

| Signature | Use when |
|-----------|----------|
| `testBodyFromFile("file.json")` | No modifications needed |
| `testBodyFromFile("file.json") { json -> ... }` | Modifying JSON fields inline |
| `testBodyFromFile("file.json", replacements)` | String-level find/replace with `ResponseReplacement` |

## Request Matchers

Use `RequestMatchers` (from `com.stripe.android.networktesting.RequestMatchers`) to validate request body parameters. Import the matchers you need statically:

```kotlin
import com.stripe.android.networktesting.RequestMatchers.bodyPart
import com.stripe.android.networktesting.RequestMatchers.hasBodyPart
import com.stripe.android.networktesting.RequestMatchers.not

networkRule.checkoutConfirm(
    bodyPart("expected_amount", "5099"),
    not(hasBodyPart("save_payment_method")),
) { response ->
    response.testBodyFromFile("checkout-session-confirm.json")
}
```

## Enqueue Responses Next to Their Trigger

Enqueue each response immediately before the operation that causes its request. Do not group responses for separate
operations at the beginning of a test; doing so obscures which action needs each mock and makes request ordering harder
to review.

```kotlin
// Initial load request.
networkRule.enqueue(consumerLookupMatcher) { response ->
    response.testBodyFromFile("consumer-session-lookup-success.json")
}
testContext.launch()

// Requests triggered by confirmation and its subsequent refresh.
networkRule.checkoutConfirm { response ->
    response.testBodyFromFile("checkout-session-confirm.json")
}
networkRule.checkoutInit { response ->
    response.testBodyFromFile("checkout-session-init.json")
}
page.clickConfirm()
```

When one user action triggers several requests asynchronously, enqueue all of those responses immediately before that
action, keeping them in the same order as the expected requests.

### bodyPart Encoding

`bodyPart()`, `hasBodyPart()`, and `query(name, value)` auto-decode both the matcher arguments and the request body before comparing. Use plain readable strings — `urlEncode()` is unnecessary:

```kotlin
// Keys with brackets and values with special characters — just use plain strings
bodyPart("billing_details[email]", "[email protected]")
bodyPart("billing_details[address][line1]", "123 Main St")
bodyPart("payment_method_data[allow_redisplay]", "unspecified")

// urlEncode() still works (backward compatible) but is unnecessary
// bodyPart(urlEncode("billing_details[email]"), urlEncode("[email protected]"))
```

### Mismatch Debugging

When a request doesn't match a mock, the error message shows per-matcher diagnostics with the nearest-miss mock:

```
POST https://localhost/v1/payment_intents/pi_123/confirm
  Body params: {billing_details[email][email protected], payment_method=pm_123}
  Nearest mock: composite(path(/v1/confirm), bodyPart(billing_details[email], [email protected]))
    + PASS: path(/v1/confirm)
    + PASS: method(POST)
    - FAIL: bodyPart(billing_details[email], [email protected])
```

See `PaymentSheetBillingConfigurationTest.kt` for more examples.

More from stripe/stripe-android

SkillDescription
compose-testsUse when writing Compose UI tests in stripe-android — covers composeRule setup, Robolectric annotations, node assertions, and test tag patterns
delegate-low-reasoning-workUse when running routine Gradle tests, builds, lint, formatting, generated-output checks, or verbose output collection in stripe-android. Delegate independent low-judgment work to a low-cost subagent to preserve the main context.
fixing-flakesReproduce, diagnose, and fix flaky stripe-android tests with focused, repeated validation and evidence-driven synchronization or determinism fixes. Use for intermittent unit, coroutine, Robolectric, Compose, screenshot, instrumentation, Gradle Managed Device, PaymentSheet E2E, Chrome, WebView, UiAutomator, or Espresso failures, while ensuring temporary ShampooRule edits never enter commits or pull requests.
parameterized-testsUse when writing, converting, or reviewing parameterized JVM, Robolectric, instrumentation, or Paparazzi tests in stripe-android. Covers TestParameterInjector runners, source-set dependencies, method versus class parameters, value providers, case matrices, and stable test names.
screenshot-testsUse when writing or running Paparazzi screenshot tests in stripe-android — covers PaparazziRule setup, recording/verifying commands, and test structure
write-unit-testsUse when writing or structuring unit tests or fake test implementations in stripe-android — covers scenarios, Turbine interaction tracking and Flow testing, and Truth assertions