Contents

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)

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.

Package Metadata

Repository: angu-software/NetworkSpyKit

Stars: 7

Forks: 0

Open issues: 3

Default branch: main

Primary language: swift

License: MIT

README: README.md