angu-software/NetworkSpyKit
A lightweight, thread-safe HTTP spy and stub tool for testing code that performs network requests in Swift.
β Features
- π« Never touches the real network
- π§ͺ Spy on requests (headers, body, URL, method)
- π Stub custom responses on a per-request basis
- π§΅ Thread-safe and safe for parallel test execution
- β Built-in teapot response for fun (and HTTP 418 awareness)
π§© Integration
NetworkSpy works with any network clients which are URLSession-based.
1. Inject NetworkSpy.sessionConfiguration into your networking stack or library.
URLSession
import Foundation
import NetworkSpyKit
let networkSpy = NetworkSpy(sessionConfiguration: .default)
let networkClient = URLSession(configuration: networkSpy.sessionConfiguration)import Alamofire
import NetworkSpyKit
let networkSpy = NetworkSpy(sessionConfiguration: .af.default)
let networkClient = Alamofire.Session(configuration: sessionConfiguration)import Foundation
import OpenAPIRuntime
import OpenAPIURLSession
import NetworkSpyKit
let networkSpy = NetworkSpy(sessionConfiguration: .default)
let session = URLSession(configuration: sessionConfiguration)
let configuration = URLSessionTransport.Configuration(session: session)
let networkClient = Client(serverURL: serverURL,
transport: URLSessionTransport(configuration: configuration))2. Provide a responseProvider closure to determine what responses should be returned.
βΉοΈ
NetworkSpys default response is418 I'm a teapot
networkSpy.responseProvider = { request in
return StubbedResponse(statusCode: 200,
data: "A pot of coffee".data(using: .utf8))
}3. Make your request through your network client
Stubbed responses never touch the real network. All requests are intercepted at the protocol layer using a
URLProtocolsubclass under the hood.
π Usage
1. Create a NetworkSpy instance
import NetworkSpyKit
struct MyNetworkingTest {
private let networkSpy = NetworkSpy(sessionConfiguration: .default)
}2. Specify a response
import NetworkSpyKit
struct MyNetworkingTest {
private let networkSpy = NetworkSpy(sessionConfiguration: .default)
func whenOrderingCoffee_itSendsACoffeeRequest() async throws {
networkSpy.responseProvider = { request in
return StubbedResponse(statusCode: 200,
data: "A pot of coffee".data(using: .utf8))
}
}
}3. Configure your URLSession based network client with NetworkSpys urlSessionConfiguration
import NetworkSpyKit
struct MyNetworkingTest {
private let networkSpy = NetworkSpy(sessionConfiguration: .default)
func whenOrderingCoffee_itSendsACoffeeRequest() async throws {
networkSpy.responseProvider = { request in
return StubbedResponse(statusCode: 200,
data: "A pot of coffee".data(using: .utf8))
}
let networkClient = makeNetworkClient(urlSessionConfiguration: networkSpy.sessionConfiguration)
}
}4. Send your request through your network client
import NetworkSpyKit
struct MyNetworkingTest {
private let networkSpy = NetworkSpy(sessionConfiguration: .default)
func whenOrderingCoffee_itSendsACoffeeRequest() async throws {
networkSpy.responseProvider = { request in
return StubbedResponse(statusCode: 200,
data: "A pot of coffee".data(using: .utf8))
}
let networkClient = makeNetworkClient(urlSessionConfiguration: networkSpy.sessionConfiguration)
try await networkClient.orderCoffee()
}
}5. Evaluate your expeced result
Inspecting the outgoing request
NetworkSpy.recordedRequests collects all send URLRequest, which we can inspect.
import NetworkSpyKit
struct MyNetworkingTest {
private let networkSpy = NetworkSpy(sessionConfiguration: .default)
func whenOrderingCoffee_itSendsACoffeeRequest() async throws {
networkSpy.responseProvider = { request in
return StubbedResponse(statusCode: 200,
data: "A pot of coffee".data(using: .utf8))
}
let networkClient = makeNetworkClient(urlSessionConfiguration: networkSpy.sessionConfiguration)
try await networkClient.orderCoffee()
#expect(networkSpy.recordedRequests.first?.url?.path == "/api/coffee/order")
}
}Evaluate response based behavior of your system
In this example we expect that orderCoffee() transforms the network response into a Beverage.aPotOfCoffee.
import NetworkSpyKit
struct MyNetworkingTest {
private let networkSpy = NetworkSpy(sessionConfiguration: .default)
func whenOrderingCoffee_itSendsACoffeeRequest() async throws {
networkSpy.responseProvider = { request in
return StubbedResponse(statusCode: 200,
data: "A pot of coffee".data(using: .utf8))
}
let networkClient = makeNetworkClient(urlSessionConfiguration: networkSpy.sessionConfiguration)
let beverage = try await networkClient.orderCoffee()
#expect(beverage == .aPotOfCoffee)
}
}π₯ Responses
NetworkSpyKit contains convenient StubbedResponses to reduce redundancy.
This includes standard HTTP responses like
| Response | StubbedResponse | |---------------------------|----------------------| | 200 OK | .ok | | 404 NOT FOUND | .notFound | | 500 INTERNAL SERVER ERROR | .internalServerError | | 501 NOT IMPLEMENTED | .notImplemented |
See CommonResponses for more convenient response implementations.
In addition it provides a convenient way to create json responses
- Use
StubbedResponse.json(statusCode:_:jsonFormattingOptions:)for supplying anEncodabletype as JSON payload in the responses body.
> To ensure your Encodable types encode deterministically the default jsonFormattingOptions contains the .sortedKeys option.
- Use
StubbedResponse.json(statusCode:jsonData:)for supplying raw JSON data in a response body. - Use
StubbedResponse.json(statusCode:jsonString:)for supplying a JSON string in a response body.
Example Creating a 200 OK JSON response from an Encodable model:
let encodableModel = YourModelConformingToEncodable()
let response = NetworkSpy.StubbedResponse.json(200, encodableModel)Example Creating a 404 Not Found response with a JSON error body:
let response = NetworkSpy.StubbedResponse.json(statusCode: 404,
jsonString: "{\"error\":\"Not found\"}")β Teapot Response (Just for Fun)
NetworkSpys default response is 418 I'm a teapot
let networkSpy = NetworkSpy()
networkSpy.responseProvider = { _ in .teaPot() }Returns:
418 I'm a teapot
Content-Type": "application/json"
{"error": "I'm a teapot"}Because Hyper Text Coffee Pot Control Protocol is real. Sort of.
π§΅ Thread Safety
NetworkSpyuses an internal serial queue to synchronize access.- You can safely use multiple spies in parallel or across test targets.
- Isolated by using unique headers to associate intercepted requests with the correct
NetworkSpyinstance.
π¦ Installation
Swift Package Manager
Add the following to your Package.swift:
.package(url: "https://github.com/yourusername/NetworkSpyKit.git", from: "1.0.0")Then import it where needed:
import NetworkSpyKitCocoaPods
Add the following line to your Podfile:
pod 'NetworkSpyKit'Then run:
pod installπ License
MIT License. See LICENSE for details.
Package Metadata
Repository: angu-software/NetworkSpyKit
Stars: 7
Forks: 0
Open issues: 3
Default branch: main
Primary language: swift
License: MIT
README: README.md