NetworkSpyKit

1.1.0

A lightweight, thread-safe HTTP spy and stub tool for testing code that performs network requests in Swift.
angu-software/NetworkSpyKit

What's New

1.1.0

2025-10-29T10:51:50Z
  • Add convenient StubbedResponses containing some standard responses like 200 OK or 404 NOT FOUND as well as responses that contain JSON payload

Full Changelog: 1.0.0...1.1.0

๐Ÿ•ต NetworkSpyKit

Run Tests

NetworkSpyKit is a lightweight, thread-safe HTTP spy and stub tool for testing code that performs network requests in Swift.

It allows you to:

  • Record outgoing URLRequests
  • Return predefined or dynamic stubbed responses
  • Assert request behavior without hitting the real network
  • Keep your tests fast, isolated, and deterministic

โœ… 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)

Alamofire

import Alamofire

import NetworkSpyKit

let networkSpy = NetworkSpy(sessionConfiguration: .af.default)

let networkClient = Alamofire.Session(configuration: sessionConfiguration)

OpenAPIURLSession

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 is 418 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 URLProtocol subclass 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 an Encodable type 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

  • NetworkSpy uses 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 NetworkSpy instance.

๐Ÿ“ฆ 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 NetworkSpyKit

CocoaPods

Add the following line to your Podfile:

pod 'NetworkSpyKit'

Then run:

pod install

๐Ÿ“„ License

MIT License. See LICENSE for details.

Description

  • Swift Tools 6.1.0
View More Packages from this Author

Dependencies

  • None
Last updated: Sun Nov 16 2025 19:07:13 GMT-1000 (Hawaii-Aleutian Standard Time)