Skip to content

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

License

Notifications You must be signed in to change notification settings

angu-software/NetworkSpyKit

Repository files navigation

πŸ•΅ 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.

About

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

Resources

License

Stars

Watchers

Forks

Packages

No packages published