{"id":30703244,"url":"https://github.com/bstien/httpmock","last_synced_at":"2025-09-02T16:57:57.644Z","repository":{"id":312408733,"uuid":"1045845177","full_name":"bstien/HTTPMock","owner":"bstien","description":"Lightweight HTTP mocking for Swift","archived":false,"fork":false,"pushed_at":"2025-08-30T11:50:04.000Z","size":180,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-08-30T12:30:06.872Z","etag":null,"topics":["dsl","integration-testing","mock","networking","swift","swift-testing","unit-testing","urlsession","xctest"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bstien.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-08-27T19:49:48.000Z","updated_at":"2025-08-30T11:49:27.000Z","dependencies_parsed_at":"2025-08-30T12:30:08.992Z","dependency_job_id":"0fe037ea-a680-4baa-98ff-6c339e1e2ef0","html_url":"https://github.com/bstien/HTTPMock","commit_stats":null,"previous_names":["bstien/httpmock"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/bstien/HTTPMock","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bstien%2FHTTPMock","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bstien%2FHTTPMock/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bstien%2FHTTPMock/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bstien%2FHTTPMock/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bstien","download_url":"https://codeload.github.com/bstien/HTTPMock/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bstien%2FHTTPMock/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":273317764,"owners_count":25084037,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-02T02:00:09.530Z","response_time":77,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dsl","integration-testing","mock","networking","swift","swift-testing","unit-testing","urlsession","xctest"],"created_at":"2025-09-02T16:57:55.238Z","updated_at":"2025-09-02T16:57:57.629Z","avatar_url":"https://github.com/bstien.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n    \u003cimg width=\"1280px\" src=\"assets/logo.png\"\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n    \u003c!-- Unit Tests CI --\u003e\n    \u003ca href=\"https://github.com/bstien/HTTPMock/actions/workflows/unit-tests.yml\"\u003e\n        \u003cimg src=\"https://github.com/bstien/HTTPMock/actions/workflows/unit-tests.yml/badge.svg\" alt=\"Unit Tests\"\u003e\n    \u003c/a\u003e\n    \u003c!-- License --\u003e\n    \u003ca href=\"https://github.com/bstien/HTTPMock/blob/main/LICENSE\"\u003e\n        \u003cimg src=\"https://img.shields.io/github/license/bstien/HTTPMock.svg\" alt=\"License\"\u003e\n    \u003c/a\u003e\n    \u003c!-- SwiftPM --\u003e\n    \u003cimg src=\"https://img.shields.io/badge/SwiftPM-compatible-orange.svg\" alt=\"SwiftPM Compatible\"\u003e\n    \u003c!-- Swift Version --\u003e\n    \u003cimg src=\"https://img.shields.io/badge/Swift-6.0+-brightgreen.svg\" alt=\"Swift 6.0+\"\u003e\n\u003c/p\u003e\n\nA tiny, test-first way to mock `URLSession` — **fast to set up, easy to read, zero test servers**. Queue responses for specific hosts/paths (and optional query params), then run your code against a regular `URLSession` that returns exactly what you told it to.\n\n\u003e **Design goals**: simple, explicit and ergonomic for everyday tests or prototyping. No fixtures or external servers. Just say what a request should get back.\n\n## Highlights\n- **Two ways to add mocks**: a **clean DSL** or **single registration methods** — use whichever reads best for your use case.\n- **Instance or singleton**: you can either use the singleton `HTTPMock.shared` or create separate instances with `HTTPMock()`. Different instances have separate response queues.\n- **Provides a real `URLSession`**: inject `HTTPMock.shared.urlSession` or your own instance's `urlSession` into the code under test. \n- **Precise matching**: host + path, plus optional **query matching** (`.exact` or `.contains`).\n- **Headers support**: define headers at the host or path, with optional **cascade** to children when using the DSL.\n- **FIFO responses**: queue multiple responses and they'll be served in order.\n- **Passthrough networking**: configure unmocked requests to either return a hardcoded 404 or be passed through to the network.\n- **File-based responses**: serve response data directly from a file on disk.\n\n## Installation (SPM)\nAdd this package to your test target:\n\n```swift\n.package(url: \"https://github.com/bstien/HTTPMock.git\", from: \"0.0.3\")\n```\n\n## Quick start\n\n### Option 1 — Imperative\n```swift\nimport HTTPMock\n\n// 1) Queue responses for a specific host + path\nHTTPMock.shared.addResponses(\n    forPath: \"/user\",\n    host: \"api.example.com\",\n    responses: [\n        .encodable(User(id: 1, name: \"Alice\")), // defaults to .ok + application/json\n        .empty(status: .notFound)               // the second call gets 404\n    ]\n)\n\n// 2) Use the session in your code under test\nlet session = HTTPMock.shared.urlSession\nlet url = URL(string: \"https://api.example.com/user\")!\nlet (data, response) = try await session.data(from: url)\n```\n\nYou can also use the imperative **builder** variant for readability:\n```swift\nHTTPMock.shared.addResponses(forPath: \"/user\", host: \"api.example.com\") {\n    MockResponse.encodable(User(id: 1, name: \"Alice\"))\n    MockResponse.empty(status: .notFound)\n}\n```\n\n### Option 2 — Declarative DSL via result builder\n```swift\nHTTPMock.shared.registerResponses {\n    Host(\"api.example.com\") {\n        Path(\"/user\") {\n            MockResponse.encodable(User(id: 1, name: \"Alice\"))\n            MockResponse.empty(status: .notFound)\n        }\n    }\n}\n```\n\nBoth approaches are equivalent — pick what suits your use case. **Responses are consumed FIFO** for each queue matching host, path and query parameters.\n\n## Headers (with optional cascade)\n```swift\nHTTPMock.shared.registerResponses {\n    Host(\"api.example.com\") {\n        Headers([\"X-Env\": \"Test\"], cascade: true) // cascades to all child paths\n\n        Path(\"/profile\") {\n            MockResponse.encodable(Profile(...)) // inherits X-Env\n        }\n\n        Path(\"/admin\") {\n            Headers([\"X-Env\": \"AdminOnly\"], cascade: false)\n            MockResponse.empty(status: .unauthorized) // X-Env applies only here\n\n            Path(\"/audit\") {\n                MockResponse.encodable(Audit(...)) // does NOT inherit AdminOnly\n            }\n        }\n    }\n}\n```\n- Later headers in the **same scope** override earlier ones on the same key.\n- **Response headers** override inherited headers on conflict.\n\n## Query parameters\nQueries are **path-local** (not inherited). You can require exact matches or require a set of params to exist on the request.\n\n```swift\n// Exact: only these params are accepted.\nPath(\"/search\", query: [\"q\": \"swift\", \"page\": \"1\"], matching: .exact) {\n    MockResponse.plaintext(\"ok-exact\")\n}\n\n// Contains: these params must match; others are ignored\nPath(\"/search\", query: [\"q\": \"swift\"], matching: .contains) {\n    MockResponse.plaintext(\"ok-contains-1\")\n    MockResponse.plaintext(\"ok-contains-2\")\n}\n```\n\n## File-based responses\nServe response data directly from a file on disk. Useful for pre-recorded and/or large responses. Either specify the `Content-Type` manually, or let it be inferred from the file.\n\n```swift\nHTTPMock.shared.registerResponses {\n    Host(\"api.example.com\") {\n        Path(\"/data\") {\n            // Point to a file in the specified `Bundle`.\n            MockResponse.file(named: \"response\", extension: \"json\", in: Bundle.main)\n            \n            // Load the contents of a file from a `URL`.\n            MockResponse.file(url: urlToFile)\n        }\n    }\n}\n```\n\nThe file path is relative to the current working directory or absolute. This allows you to serve JSON, images, or any other file content as the response body.\n\n## Response lifetime\nEach response can be configured with a `lifetime` parameter to control how many times it is served before being removed from the queue. The default value of the parameter is `.single`.\n\n- `.single`: The response is served once, then removed from the queue. This is the default.\n- `.multiple(Int)`: The response is served the specified number of times, then removed from the queue.\n- `.eternal`: The response is never removed and is served indefinitely.\n\nExample:\n\n```swift\nMockResponse.plaintext(\"served once\", lifetime: .single)\nMockResponse.plaintext(\"served three times\", lifetime: .multiple(3))\nMockResponse.plaintext(\"served forever\", lifetime: .eternal)\n```\n\n## Response delivery\nEach response can optionally be given a `delivery` parameter that controls when the response is delivered to the client. The default value of the parameter is `.instant`.\n\n- `.instant`: The response is delivered immediately (default behavior).\n- `.delayed(TimeInterval)`: The response is delayed and delivered after the specified number of seconds.\n\nExample:\n\n```swift\nMockResponse.plaintext(\"immediate response\", delivery: .instant)\nMockResponse.plaintext(\"delayed response\", delivery: .delayed(2.0)) // delivered after 2 seconds\n```\n\n## Handling unmocked requests\nBy default, unmocked requests return a hardcoded 404 response with a small body. You can configure `HTTPMock.unmockedPolicy` to control this behavior, choosing between returning a 404 or allowing the request to pass through to the real network. The default is `notFound`, aka. the hardoced 404 response.\n\n```swift\n// Default: return a hardcoded 404 response when no mock is registered for the incoming URL.\nHTTPMock.shared.unmockedPolicy = .notFound\n\n// Alternative: let unmocked requests hit the real network.\n// This can be useful if you're doing integration testing and only want to mock certain endpoints. \nHTTPMock.shared.unmockedPolicy = .passthrough\n```\n\nPassthrough is useful for integration-style tests where only some endpoints need mocking, but it is not recommended for strict unit tests.\n\n## Resetting between tests\nUse these in `tearDown()` or in individual tests:\n```swift\n// Remove all queued responses and registrations.\nHTTPMock.shared.clearQueues()\n\n// Remove all paths/responses for a host you've already registered.\nHTTPMock.shared.clearQueue(forHost: \"domain.com\")\n```\n\n## Singleton vs. separate instances\nYou can use the global singleton `HTTPMock.shared` for simplicity in most cases. However, if you need isolated queues to, for example, run parallel tests or maintain different mock configurations you can create separate instances with `HTTPMock()`.\n\nEach instance maintains their own queue and properties, and they have no connection to each other.\n\nExample:\n\n```swift\n// Using the singleton\nHTTPMock.shared.registerResponses {\n    Host(\"api.example.com\") {\n        Path(\"/user\") {\n            MockResponse.plaintext(\"Hello from singleton!\")\n        }\n    }\n}\nlet singletonSession = HTTPMock.shared.urlSession\n\n// Using a separate instance.\nlet mockInstance = HTTPMock()\nmockInstance.registerResponses {\n    Host(\"api.example.com\") {\n        Path(\"/user\") {\n            MockResponse.plaintext(\"Hello from instance!\")\n        }\n    }\n}\nlet instanceSession = mockInstance.urlSession\n```\n\n\n## FAQs\n**Can I run tests that use `HTTPMock` in parallel?**  \nPreviously, only a single instance of `HTTPMock` could exist, so tests had to be run sequentially. Now, you can create multiple independent `HTTPMock` instances using `HTTPMock()`, allowing parallel tests or separate mock configurations. The singleton `HTTPMock.shared` still exists for convenience.\n\n**Can I use my own `URLSession`?**  \nYes — most tests just use `HTTPMock.shared.urlSession`. If your code constructs its own session, inject `HTTPMock.shared.urlSession` or your own instance's `urlSession` into the component under test.\n\n**Is order guaranteed?**  \nYes, per (host, path, [query]) responses are popped in **FIFO** order.\n\n**What happens if a request is not mocked?**  \nBy default, unmocked requests return a hardcoded \"404 Not Found\" response. You can configure `HTTPMock`'s `UnmockedPolicy` to instead pass such requests through to the real network, allowing unmocked calls to succeed.\n\n## Example response helpers\nThese are available as static factory methods on `MockResponse` and can be used directly inside a `Path` or `addResponses` builder:\n\n```swift\nMockResponse.encodable(T, status: .ok, headers: [:])\nMockResponse.dictionary([String: Any], status: .ok, headers: [:])\nMockResponse.plaintext(String, status: .ok, headers: [:])\nMockResponse.file(named: String, extension: String, in: Bundle, status: .ok, headers: [:])\nMockResponse.file(URL, status: .ok, headers: [:])\nMockResponse.empty(status: .ok, headers: [:])\n```\n\nFor example:\n\n```swift\nPath(\"/user\") {\n    MockResponse.encodable(User(id: 1, name: \"Alice\"))\n    MockResponse.empty(status: .notFound)\n}\n```\n\n## Notes\n- Intended for **tests** (unit/integration/UI previews), not production networking.\n- Internally uses a custom `URLProtocol` to intercept requests and match incoming requests to a specific mocked response.\n- Thread-safe queueing and matching by host + path + optional query.\n- Supports passthrough networking or 404 for unmocked requests, configurable via `HTTPMock.unmockedPolicy`.\n\n## Goals\n- [X] Allow for passthrough networking when mock hasn't been registered for the incoming URL.\n- [X] Let user point to a file that should be served.\n- [X] Set delay on requests.\n- [X] Create separate instances of `HTTPMock`. The current single instance requires tests to be run in sequence, instead of parallel.\n- [ ] Let user configure a default \"not found\" response. Will be used either when no matching mocks are found or if queue is empty.\n- [ ] Does arrays in query parameters work? I think they're being overwritten with the current setup.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbstien%2Fhttpmock","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbstien%2Fhttpmock","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbstien%2Fhttpmock/lists"}