Skip to main content
A check names the state the device is in while it runs, the network the app sees, and what the app must send and store. Mobster sets that state before the launch and puts it back when the run ends.

Set the device’s state

Add conditions to an existing check file. This example runs it in dark appearance with larger text:
appearance, content_size and locale take a list as well as one value. A check with appearance: [light, dark] and content_size: [large, accessibility-large] runs four times under mobster test, once for each combination, named “Home [light, large]” and so on (at most 24 combinations). mobster verify runs one device state, so it refuses a check with lists and says to use mobster test. Mobster checks the block when the file loads. A typo, a missing file or a value out of range stops the check before anything runs, and the message says what is wrong and what to write. A condition that can’t be set when the check runs makes it couldnt_run (class environment): the check never runs in the wrong state. When the run ends, Mobster puts back the appearance, content size, contrast, location or route, status bar, permissions and Face ID enrollment it changed. Photos and videos added with media stay in the simulator’s library. Erase the simulator to clear them. status_bar data_network and wifi_mode change only what the status bar shows, not the connection. On a real iPhone, Mobster sets only appearance and locale. network and stubs are refused there, and the check ends as couldn’t run. Everything else changes a phone’s state (its location, its permissions, its photos) or sends to a real token, so Mobster skips it. reset is not a condition: it is the check’s own key, with the levels none, data and reinstall (Reset levels). Each result in results.json has conditions.applied and conditions.skipped (each skipped key and why), and the report notes them.

Slow the network and stub requests

conditions.network limits the app’s connection while the check runs, and stubs answers named requests with fixed responses. Both work on a simulator only.
A stall stands for a lost packet and its retransmission. flaky draws its stalls and resets from a fixed seed, so a check meets the same failures on every run unless its requests race each other. These profiles aren’t Apple’s Network Link Conditioner profiles. A stub matches on host (a name, or *.example.com), path_prefix (default /) and method (default any), and the first match wins. respond takes status (default 200), a body from json or from file (a path relative to the project, up to 8 MiB) and delay_ms (up to 30,000). A request no stub matches goes to the real server, shaped by the profile. offline and stubs together are an error, because nothing can answer on a link that refuses everything. Before the app launches, Mobster starts a proxy on 127.0.0.1 and launches the app with a small library (built with Xcode’s compiler the first time and cached in ~/Library/Caches/Mobster/netshim) that points the app’s URLSession at it. Mobster changes no rule, setting or file on your Mac, asks for no password and doesn’t touch other apps’ traffic. The proxy lives as long as the run.
  • The app keeps the library each time the run opens it, after a relaunch or a quit, and a library you name in the check’s DYLD_INSERT_LIBRARIES loads beside it.
  • A link that opens in the app while it has no process launches the app that way first: the check’s open_url, the open_url tool, and, on a simulator, a link the model opens in one of the URL schemes the app declares (CFBundleURLTypes), or one Mobster maps to an app it knows, such as Settings.
  • When iOS starts the app some other way, such as a tap on its Home Screen icon or a universal link, the app runs without the library, and the run ends needs_review, class unlaunched_process.
  • Each run writes what the proxy saw to network.json in its run folder (method, host, path without the query, status, bytes, time, and whether the request was passed, stubbed, tunneled or refused). conditions.network in results.json repeats the first 200 rows.
The proxy reaches every URLSession (shared, default and ephemeral, over http and https), so it covers Swift and Objective-C apps, Alamofire and React Native’s fetch. It doesn’t reach:
  • The app’s view of the link. NWPathMonitor, Reachability and the status bar read your Mac’s connection, so an offline screen that appears only when the system reports no network stays hidden. Under offline, an http request fails with NSURLErrorNetworkConnectionLost (-1005) and an https request with error 310 in kCFErrorDomainCFNetwork, never NSURLErrorNotConnectedToInternet (-1009).
  • Traffic that doesn’t use URLSession: Flutter’s dart:io, gRPC, Cronet, Firebase’s own transport, raw sockets and QUIC. When no request reaches the proxy, network.json says so.
  • Web views, other apps and system services.
  • An app that sets its own proxy in its URLSessionConfiguration.
An https stub needs the app to trust a certificate Mobster makes for the run. Mobster adds it to the simulator’s trust store with simctl keychain add-root-cert. The run’s private key is deleted when the run ends, and the public certificate stays in the simulator. Apps that pin certificates refuse a stubbed host, and the check fails with the app’s own error. An https host without a stub passes through as a tunnel Mobster never opens, so pinned apps keep working under network: alone.

Assert what the app sent and stored

expect: can also judge what the app sent over the network and what it wrote to disk, so a check can prove that “Add to cart” reached the server without reading a second screen. This works inside canvases and web views where the accessibility tree is empty, as long as the traffic uses URLSession. Set MOBSTER_EXPERIMENTAL_NETWORK=1, or Mobster refuses a check that uses it. It runs on a simulator only. On a real iPhone the check ends as couldn’t run.
path is a glob (* matches anything, including /) on the path, or a /regex/. A glob without ? ignores the query string. A body or response is a case-insensitive substring, or a /regex/. Mobster reads gzip, deflate and chunked response bodies, and the first 64 KiB of each body. A request that matches is the evidence: POST api.example.com/v1/cart -> 200 at 4.2 s in the report, and wire.json in the run folder with the request’s headers (authorization, cookie, token and key headers are masked, other values go through the secret filter) and the first 200 characters of its body and response. A storage change shows the diff of a text or plist file, or the first rows of a query, masked the same way. results.json repeats this in the assertion’s matches.
  • Mobster opens an https connection only to a host a request or no_request names together with a method, path, status, body or response, because opening it needs the run’s certificate. Name the host. Any other https connection is a tunnel: the assertions see its host, not its paths.
  • A no_request with a method, path or body and no host can’t be decided while the app has an unopened https connection, and fails with the host to name. With a host, it is decided on that host alone.
  • If no request from the app reached the proxy at all, request and no_request both fail: the app may not use URLSession. Pair a no_request with a request or a screen assertion that proves the app was routed.
  • The proxy records every request from launch to the end of the run, not only those after the steps.
  • Pinned apps refuse the certificate for an opened host, as with stubs.
  • storage compares the app’s data container at the launch screen with the container at the end. Mobster reads it in place with simctl get_app_container. Preferences the app hasn’t flushed to disk aren’t there yet.