Skip to content

iOS · Mocking · GraphQL

Mock an API response in the iOS Simulator without changing code

Turn a captured request into a mock, edit the JSON, and test 500s, 401s, empty lists, slow responses and single GraphQL operations, then share the mocks with your team.

Subcloak10 min read

Cloak Proxy's Map Local window with rule folders Checkout, Declined card, Empty cart and Onboarding, rule sets such as Payment declined, and a close-up of a rule's conditions: GraphQL operation is CreateOrder, a JSON body value and an X-App-Version header
On this page
  1. Before you start
  2. How Map Local works
  3. Mock a response from a captured request
  4. Recipes for the states you need to test
  5. A server error (500)
  6. An expired session (401)
  7. An empty list
  8. A slow response
  9. A broken response
  10. Mock one GraphQL operation
  11. Record a whole API with Mock Sets
  12. Switch between scenarios with rule sets
  13. Share mocks with your team
  14. When stubs in code are the better choice
  15. Turn the mocks off
  16. Sources
  17. FAQ

You need to see how your app’s order screen looks when the list is empty. Or when the server returns a 500. Or when the session has expired and the API says 401. Getting the real backend into those states is slow, and adding #if DEBUG stubs to the app means changing code you’ll have to remember to remove.

A debugging proxy can answer the request instead. The app sends its normal request, the proxy replies with the JSON you wrote, and the app never knows the difference. This guide shows how to do that in the iOS Simulator with Map Local in Cloak Proxy, our free Mac app, and when stubs in code are the better choice.

Before you start

You need Cloak Proxy capturing the Simulator, with your API’s host decrypted. That takes about five minutes:

  1. Turn on the macOS proxy with ⇧ ⌘ P. Simulators use the Mac’s network settings.
  2. Click Devices in the toolbar, choose iOS Simulator, and click Install Certificate on All Booted Simulators. Restart your app in the simulator.
  3. Add your API’s host, such as api.example.com, in Cloak Proxy › Settings… › SSL Proxying.

If you haven’t done this before, our guide to seeing your iPhone app’s network traffic walks through it, including real devices.

How Map Local works

Map Local is a list of rules. Each rule matches requests by method and URL, and optionally by app, header, query parameter, JSON body value or GraphQL operation. When a request matches, Cloak Proxy answers it with the rule’s status code, headers and body. The server is never contacted.

A matching rule answers the request on your Mac. Requests that match no rule carry on as usual.

Map Local runs before Mock Sets and Map Remote, so a matching rule always wins. The mocked response still passes through the tools that act on responses, so you can slow it down with Network Conditions or pause it at a breakpoint.

Mock a response from a captured request

The fastest way to write a mock is to start from the real thing.

  1. Use the screen in your app once, so the request appears in Cloak Proxy’s traffic list.
  2. Right-click the request and choose Tools › Map Local…. Cloak Proxy creates a rule that matches the request’s method and URL, copies the captured response into it, turns Map Local on and opens the rule.
  3. Under Respond with, edit the status code, headers or body.
  4. Make the request again in the app: pull to refresh, or leave the screen and come back. The row in the traffic list now has a Map Local badge.
A Map Local rule for GET https://api.example.com/v1/users with four numbered markers: 1 the method and URL matcher, 2 the Inline, File and Directory choice under Respond with, 3 the status code 200, 4 the response headers copied from the capture
A rule made from a captured request: the matcher (1), where the response comes from (2), the status (3) and the headers copied from the real response (4). The body editor is below.

A few details save confusion later:

  • The query string is ignored. Without Include subpaths, the rule matches the same path with any query, so /v1/orders?page=2 uses the same mock as /v1/orders.
  • Only useful headers are copied: Content-Type, Cache-Control, Set-Cookie, Location, Content-Disposition, WWW-Authenticate, Retry-After and any Access-Control-* headers. Server and CDN headers are left out.
  • The body is decompressed and JSON is pretty-printed, so you can edit it as text. Cloak Proxy sets Content-Length for you, so don’t add one.
  • Caches can hide your change. If the app keeps showing the old data, turn on Tools › No Caching so it asks again instead of reusing a cached copy.

Recipes for the states you need to test

Each recipe is a change to the same rule. Make one rule per state and switch them on and off; later, folders and rule sets do the switching for you.

A server error (500)

Set Status code to 500 and replace the body:

{ "error": "internal", "message": "Something went wrong on our side." }

Check that the app shows a friendly error, keeps the last good data if it should, and offers a retry.

An expired session (401)

Set the status to 401 and add a header your client might read:

WWW-Authenticate: Bearer error="invalid_token"

A good app refreshes its token once and repeats the request, or signs the user out cleanly. Watch the traffic list: if you see the same 401 request repeated dozens of times, you’ve found a retry loop.

An empty list

Keep the status at 200 and return an empty collection in the shape your API uses:

{ "orders": [], "next": null }

This is where empty-state screens and “Load more” buttons tend to break.

A slow response

Map Local answers instantly, which hides loading states. Add a Network Conditions rule for the same host: right-click the request and choose Tools › Network Conditions…. Mocked responses skip the upload delay but still get the download delay, which is half the profile’s latency plus the body’s transfer time. So for a delay of about three seconds, type a custom Latency of 6000 ms. Now you can see your spinner, skeleton screen or timeout.

A broken response

Leave a brace out of the body. The editor warns Invalid JSON, but the rule sends exactly what you wrote. A crash here means the app trusts its decoder too much.

Mock one GraphQL operation

GraphQL sends every operation to one URL, often POST /graphql, so a rule that matches only the URL would answer every query and mutation with the same body. Conditions fix this.

One URL, several operations. The condition picks out the one you want to mock.

Right-click a GraphQL request and choose Tools › Map Local…. The new rule already has a GraphQL operation is … condition and is named after the operation, such as mutation CreateOrder. To narrow it further, click Add Condition and choose JSON Body Value (JSONPath), Request Header, Query Parameter, Body Text or GraphQL Operation Type.

A Map Local rule's Conditions section with three conditions: GraphQL operation is CreateOrder, JSON body $.variables.input.payment is card, and header X-App-Version matches regex ^4\., above a line reading Matches 1 of 82 captured requests
Three conditions: the operation, a value in the request's variables, and the app version. The line underneath counts how many captured requests the rule would match.

The rule above answers only the CreateOrder mutation, only when the payment method is a card, and only from app versions starting with 4. GraphQL servers usually report failures with a 200 and an errors array, so the mock does too:

{
  "data": { "createOrder": null },
  "errors": [
    {
      "message": "Your card was declined.",
      "path": ["createOrder"],
      "extensions": { "code": "PAYMENT_DECLINED" }
    }
  ]
}

Cloak Proxy recognizes this shape. The row shows a red 200 ✕, and the inspector shows the error above the response.

Record a whole API with Mock Sets

Map Local is for the one response you want to change. When you want everything the way it was at one moment, for example to demo the app on a plane or keep working while staging is down, use a mock set.

Use the app so Cloak Proxy captures the screens you need. Then right-click the domain in the sidebar and choose Mock api.example.com Offline (the item shows your domain’s name). Every captured response from that host becomes a recording, and requests are answered from it.

The Mock Sets window with a set named GET api.example.com/v1/users, the option Offline: answer unrecorded requests to these hosts with 404 marked 1, and one recorded response for https://api.example.com/v1/users?page=1&limit=2&sort=name with status 200
With Offline on (1), requests you didn't record get a 404 instead of reaching the server.

With Offline on, a request you didn’t record gets a 404 with the header X-Cloak-Proxy: mock-miss, so you can spot screens you missed. GraphQL recordings remember their operation, so GetUser gets the GetUser recording and GetCart the GetCart one, even though both went to /graphql. You can edit any recorded status, header or body in the window.

Switch between scenarios with rule sets

After a week you’ll have rules for “card declined”, “empty cart”, “first launch” and more. Turning them on and off one by one gets old. Put each scenario’s rules in a folder, then save folders as a rule set.

The Debugging Tools window with Map Local selected, a Checkout folder containing a Declined card folder marked 1, other folders Empty cart and Onboarding, and under Rule Sets the active set Payment declined marked 2, with Empty cart and New user below it
Folders group rules (1). A rule set (2) turns its folders on and every other set's folders off.
  1. In the rule list, click the new-folder button and drag the rules into it.
  2. Turn on the folders that make up a scenario, in any tools: for example a Map Local folder that declines the card and a Network Conditions folder that slows checkout.
  3. Under Rule Sets in the sidebar, click Rule Set and choose New Set from Folders That Are On…. Name it Payment declined.

Switching to a set turns on its folders, turns off folders that belong to other sets, and leaves everything else alone. You can switch from the sidebar or from the N tools on button in the bottom bar.

Adding a GraphQL operation condition to a rule, then switching from the Payment declined set to Empty cart.

Share mocks with your team

Right-click a set and choose Export Set… (or Export Folder… on a folder). You get a rules file with just those folders, their rules and the set. A teammate imports it with Tools › Import Rules…; importing an updated file again replaces the old rules instead of duplicating them.

Keep these files next to your app’s code, and the scenarios travel with the branch that needs them. In a test script, cloakctl switches sets against the running app:

cloakctl rules import mocks/payment-declined.json
cloakctl rules set activate "Payment declined"
# run your UI tests here
cloakctl rules set off

When stubs in code are the better choice

Proxy mocks are best for looking: trying a state by hand, showing a designer the error screen, letting QA walk through “payment declined” on a real build. They change nothing in the app, so they work with any build, and other people can use them without touching the code.

For automated tests, stub in code. Tests in CI should be hermetic: they shouldn’t depend on a proxy running on the machine, a trusted certificate or the order rules were switched on. Apple’s networking has a hook for this: a URLProtocol subclass, registered on the session’s configuration, can answer requests itself.

final class StubProtocol: URLProtocol {
    nonisolated(unsafe) static var stubs: [String: (status: Int, body: Data)] = [:]

    override class func canInit(with request: URLRequest) -> Bool {
        stubs[request.url?.path ?? ""] != nil
    }
    override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }

    override func startLoading() {
        let url = request.url!
        let stub = Self.stubs[url.path]!
        let response = HTTPURLResponse(url: url, statusCode: stub.status, httpVersion: "HTTP/1.1",
                                       headerFields: ["Content-Type": "application/json"])!
        client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
        client?.urlProtocol(self, didLoad: stub.body)
        client?.urlProtocolDidFinishLoading(self)
    }
    override func stopLoading() {}
}

// In a test
let config = URLSessionConfiguration.ephemeral
config.protocolClasses = [StubProtocol.self]
StubProtocol.stubs["/v1/orders"] = (500, Data(#"{"error":"internal"}"#.utf8))
let session = URLSession(configuration: config)

This needs your networking code to accept an injected URLSession, which is good design anyway. Libraries such as OHHTTPStubs wrap the same idea with a friendlier API, but check whether a dependency is still maintained: its last release, 9.1.0, is from December 2020.

A practical split: explore and design with proxy mocks, then write the cases you care about as stubbed tests. The JSON you wrote in Map Local makes a good fixture.

Turn the mocks off

Map Local keeps answering until you turn it off, even after a restart. When the app shows data you didn’t expect, look at the N tools on button in Cloak Proxy’s bottom bar: hover it to see which tools have active rules, and click to turn them off.

Sources

Checked on 19 October 2026.

FAQ

Does Map Local work on a real iPhone too?

Yes. Once the iPhone uses your Mac as its Wi-Fi proxy and trusts the certificate, Map Local rules answer its requests the same way. See how to see your iPhone app's traffic.

Will a mock affect other apps on my Mac?

Only if they call the same URL. A rule matches every app by default; type your app's name in the rule's Only these apps field to limit it, for example ShopApp.

Will my mocks still be on tomorrow?

Yes. Rules stay on after a restart until you turn them off. If the app shows stale data, check the N tools on button in Cloak Proxy's bottom bar.

Is this a replacement for stubs in my unit tests?

No. Proxy mocks are for exploring, design review and manual QA. Tests that run in CI should stub in code so they don't depend on a proxy running on the machine.

Related posts

Cloak Proxy's Failure Scenarios window with the Flaky checkout scenario on: Checkout fails twice for the CreateOrder GraphQL operation, Time out after 20 s on the payments API only the 2nd time, Cut the body to 50% on /v1/config 30% of the time, and a Trickle at 1 KB/s fault turned off. Below it, the Log tab lists what each fault did to each request.

TestingNetworking

Test how your app handles a flaky network

A checklist of network failures to cause on purpose: retries, timeouts, resets, cut-off bodies, slow or stale responses and expired sessions, locally and in CI.

· 11 min read

All posts RSS feed