network-tests

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

Generate Stripe Android network integration tests with mocked responses.

  • Enables developers to write tests requiring mocked network responses.
  • Integrates with NetworkRule and testBodyFromFile for JSON handling.
  • Selects fixture files from src/androidTest/resources by filename.
  • Delivers runnable test classes ready for instrumentation testing.
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", "session@example.com")
    }
}

// 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")
}
```

### 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]", "user@example.com")
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("user@example.com"))
```

### 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]=actual@test.com, payment_method=pm_123}
  Nearest mock: composite(path(/v1/confirm), bodyPart(billing_details[email], expected@test.com))
    + PASS: path(/v1/confirm)
    + PASS: method(POST)
    - FAIL: bodyPart(billing_details[email], expected@test.com)
```

See `PaymentSheetBillingConfigurationTest.kt` for more examples.
More from stripe/stripe-android