AppInspect 0.7.0
On this page
Panels

Response mocking

A mock rule replaces the response to a matching API call with one you define. You use it to reach the states that are hard to produce on demand — a 500, an empty list, a timeout, an endpoint the backend has not built yet — without changing your app's code or asking anyone to break a server.

What it changes, and what it does not

A mock is a real okhttp3.Response handed back from the interceptor. Your app cannot tell the difference, which is the whole point: isSuccessful, the response body, Retrofit's converters and a chained flow that only calls B after A succeeds all behave exactly as they would against the live server.

Your outgoing request is never modified. The URL, query, headers and body your app sends are the ones it actually sends. Everything in a rule's match section is a filter used to decide whether the rule applies — not data that gets transmitted. This trips people up, so it is worth saying plainly: mocking changes what comes back, nothing else.

Where rules come from

Two places, and they are meant for different situations.

SourceLives inWhy you would use it
Bundled assets/appinspect/mocks.json in your app Version-controlled, reviewable in a pull request, shared with the whole team, and already active on a fresh install with nobody opening the inspector.
Runtime Created in the Mocks panel, stored on the device Instant. No rebuild, no commit. What you reach for while you are actually debugging.

When both a bundled and a runtime rule match the same request, the runtime one wins. Rules you make on a device can be pulled back into the project later — see getting device rules into git.

The two modes

Every rule is either a short circuit or an override, and the difference is whether the real request happens at all.

SHORT_CIRCUITdefault
The network is never touched. AppInspect returns your response immediately. This is what you want for an endpoint that does not exist, for offline behaviour, or when you simply do not want the call to reach production.
OVERRIDE
The real call completes normally, and then its status, headers and body are replaced with yours. The original status is preserved in an X-AppInspect-Mock-Original-Status response header, so you can still see what the server said. Use it when the request itself has a side effect you want to keep — a write that must really happen — while testing how your UI handles a different answer.

What a rule matches on

Four conditions, and every one you leave blank matches anything. A rule therefore only needs the fields you actually care about.

methoddefault: ANY
Exact HTTP method, or ANY.
urlrequired, unless you use a query or body condition
A case-insensitive substring of the full URL, with * as a wildcard. Because it is a substring, /v1/users matches on every host — so a rule keeps working after someone switches environments, which is usually what you want.
query
Name and value pairs that must be present in the query string. Other parameters are ignored.
bodyContains
A substring the request body must contain. If the body was not captured, this condition fails closed — the rule does not fire rather than firing blindly.

What the rule sends back

A status code, a content type, any response headers you want, and a body. On top of that, two things that are hard to arrange any other way:

  • A delay. delayMillis holds the response back, which is how you catch a spinner that never stops or a race between two calls.
  • A failure. IO_EXCEPTION and SOCKET_TIMEOUT make the call fail the way a real network failure does, so you can test timeout and offline handling on a perfectly good connection. For a failure rule there is no status code to set — the field is hidden.

Committing rules with your code

A bundled file is read once, on AppInspect's background install thread, so it never blocks startup. A missing file is the normal case. A malformed one produces errors surfaced in the Mocks panel rather than an exception.

app/src/debug/assets/appinspect/mocks.json
{
  "version": 1,
  "mockingEnabled": true,
  "rules": [
    {
      "id": "orders-empty",
      "name": "Orders list — empty state",
      "enabled": true,
      "priority": 10,
      "mode": "SHORT_CIRCUIT",
      "delayMillis": 250,
      "match": { "method": "GET", "url": "/v2/orders", "query": { "page": "1" } },
      "response": {
        "status": 200,
        "contentType": "application/json",
        "headers": { "cache-control": "no-store" },
        "body": { "items": [], "total": 0 }
      }
    }
  ]
}
Use src/debug/assets, not src/main/assets

Assets are not stripped per variant the way code is. A mocks.json under src/main/ is packaged into your release APK too. Nothing there can be served in production, but the file itself — every URL and canned response body in it — ships to users and is readable by anyone who unzips the APK. A debug-only source set keeps it out entirely.

Also: rules are developer-authored configuration and are stored verbatim. Redaction applies to captured traffic, not to rules, so never put a real credential in a mock response body.

The JSON keys are shorter than the Kotlin names

Unknown keys are ignored, so a typo silently does nothing. This is the full mapping:

JSON keyMeaningIf omitted
idRule idRequired. A rule without one is skipped and reported.
nameLabel shown in the panelEmpty
enabledWhether the rule can firetrue
priorityHigher wins among rules of the same source0
modeSHORT_CIRCUIT or OVERRIDESHORT_CIRCUIT
failureNONE, IO_EXCEPTION, SOCKET_TIMEOUTNONE
delayMillisArtificial delay0, clamped to the maximum delay
match.methodHTTP methodANY
match.urlURL substring, * wildcardsMatches every URL
match.queryJSON object of name to valueMatches any query string
match.bodyContainsRequest-body substringMatches any body
response.statusStatus code, 100599200
response.contentTypeResponse content typeapplication/json
response.headersJSON object of name to valueNone
response.bodyA JSON string, or an inline object or arrayEmpty

match.query and response.headers are objects, not arrays, and keep document order. response.body accepts either a string or an inline object; the inline form is stored as compact JSON, which is usually the more readable way to write it in a file a human will review.

Working with rules in the panel

The rule list is split into runtime rules and bundled rules. Every action, and what it actually does:

ActionWhereEffect
Add ruleRuntime section headerA new rule with a generated rt-… id.
EditRuntime rule menu, or tap the rowUpdates that rule in place.
Override on this deviceBundled rule menu, or tap the rowOpens the shipped rule with every field editable and saves a runtime rule under the same id — because an app cannot write into its own APK assets.
Edit device overrideBundled rule menu, once overriddenThe same action again. The row gains an amber OVERRIDDEN badge.
Duplicate as new ruleAny rule menuA genuinely separate rule with a fresh id and priority + 1. Use it when you want the shipped rule and a variant.
Restore from mocks.jsonOverridden bundled rule menuDrops the override; the file's version serves again.
DeleteRuntime rule menuRemoves it. Not offered on bundled rows — use Restore.
Row switchAny ruleEnables or disables. On a bundled row this writes an override too, which is how a shipped rule gets silenced without editing the file.
Clear runtime rulesPanel overflowRemoves every device-made rule. Bundled rules are untouched.

An override replaces the bundled rule rather than outranking it. So turning an override off silences the rule entirely instead of letting the shipped version resurface, and an export contains one entry per rule with your edits applied — never a stale copy of the original. The override is shown inside its bundled row rather than as a separate entry, which is why the Bundled rules count always equals the number of rules in your file.

In the rule editor

The editor is split into Match (filters on the outgoing request) and Response (what your app receives instead). Nothing in the Match section is ever sent.

Only two fields are marked required: the URL pattern — unless you supply a query or body condition instead — and the status code, which disappears for failure rules. Everything else blank means "matches anything".

When several rules match

Rules are evaluated in this order, and the first match wins. They are never merged.

  1. Runtime rules, by priority descending, then most recently saved.
  2. Bundled rules, by priority descending, then file order.

Two rules with the same matcher and the same priority therefore resolve to whichever was saved last, and the loser can never fire. Rather than leave that invisible, the panel flags it with a NEVER FIRES badge. The usual cause is running Mock this response twice on the same endpoint.

Turning mocking on and off

Mocking rewrites live traffic, so it is gated more heavily than the rest of the inspector. Five checks must all pass before a rule can fire — if a rule looks right but nothing happens, work down this list.

GateSet inDefault
Build tier / FLAG_DEBUGGABLEAppInspectEnablementDebug builds only
allowResponseMockingYour install() configurationtrue
The device switch, in the Mocks panelPersisted per installUnset
"mockingEnabled"Your mocks.json, in gittrue when the file exists
The rule's own enabledFile, or the row switchtrue

The device switch is a tri-state override. Unset means "follow mocks.json", which is what lets a shipped file work on a fresh install with nobody opening the inspector. Flip the switch and the device wins; Reset to mocks.json clears the override and hands control back. With no file and no override, mocking is off.

So "mockingEnabled": true in a committed file does mean every teammate's debug build starts mocking those endpoints on first launch. That is the intent — it makes a test scenario reproducible and reviewable — and anyone can take control on their own device without touching the file.

Getting device rules back into your project

Rules built on a device are worth keeping. AppInspect mirrors the current rule set to app-scoped external storage on every change, so you can pull the file without run-as:

Terminal
# Downloads to ./mocks.json in your current directory
adb pull /sdcard/Android/data/<applicationId>/files/appinspect/mocks.json ./mocks.json

# Then move it into your module — adjust the module name
mkdir -p app/src/debug/assets/appinspect
mv mocks.json app/src/debug/assets/appinspect/mocks.json

Copy adb pull command in the panel overflow gives you that first line with your package already filled in. It deliberately targets the current directory rather than guessing a path: adb pull does not create intermediate directories, and the library cannot know your module's name. The panel also has Export rules (.json) through the share sheet and Copy all as JSON, and the Mirror file (adb pull) disclosure in the master card shows the exact on-device path.

The exported file contains one entry per rule: every runtime rule, plus every bundled rule that no runtime rule overrides. Edit a shipped rule on the device and the pulled file has your edited version, with no leftover copy of the original.

Treat the mirror file like an export

It lives on app-scoped external storage, is removed when the app is uninstalled, and is never written on a build where mocking is not allowed — but it is not app-private the way internal storage is. The security model covers what that means.

Three things that surprise people

Because a mock is a real response, almost everything behaves normally. The exceptions are worth knowing before you spend an hour on one.

  • Your mock body still has to parse. If the response model expects fields your mock leaves out, the converter throws. The chain breaks at deserialization, not at the network layer — and that is by far the most common surprise.
  • SHORT_CIRCUIT skips the rest of the chain. Interceptors registered after AppInspect's, and OkHttp's own bridge, cache, connect and call-server interceptors, do not run. Installing AppInspect last avoids this. OVERRIDE does not have the problem, because the real call completes first.
  • No cache write and no cookie storage on a short circuit, for the same reason.

One more, less obvious: application interceptors see a call once, before redirects. A SHORT_CIRCUIT rule written against the final URL of a redirect chain will not fire.

A mocked response is never silent

The worst failure mode for a tool like this is someone debugging a response that AppInspect invented. So every mocked call is labelled, in every place it appears:

  • A MOCK badge on the row and in the call detail in Network.
  • A MOCK · prefix and an amber tint on the notification.
  • An amber badge on the Mocks tab for as long as mocking is on.
  • A Mock: line, naming the rule that fired, in .txt exports.

If a response is confusing someone on your team, those badges are the first thing to check.