Skip to content

Better Stubbing Support #24

@fgeistert

Description

@fgeistert

For a project that I am working on, I am trying to test my token refresh integration with Fetch. However, I am running into some limitations with the stubbing support of Fetch.

The Stub protocol is defined as follows:

/// A `Stub` represents a network response, which can be used to imitate an API endpoint
public protocol Stub {
    
    /// HTTP status code
    typealias StatusCode = Int
    
    /// The result of the stubbed network call
    /// It can be used to return a HTTP status code with a HTTP body and HTTP headers or an error
    var result: Result<(StatusCode, Data, HTTPHeaders), Error> { get }
    
    /// The id to identify a `Stub`. It is used to match the stub to the response
    var id: UUID { get }
    
    /// The `TimeInterval` after which the stub is returned to simulate a network delay
    var delay: TimeInterval { get }
}

This is good, but does not support "dynamic" stubbing. To be more precise, I would like to adapt a stub, based on the request parameters. Here is what I would imagine a better (or more dynamic) stub to look like:

public struct StubResponse {
    let statusCode: Int
    let headers: HTTPHeaders
    let body: Data
}

public protocol Stub {
    var delay: TimeInterval { get }
    func response(for request: URLRequest) -> StubResponse
}

Using this approach, more complex stubs could be constructed. For my personal use-case, I could define a stub that returns a 401 (triggering the token refresh logic) and for the second request a regular 200.

Another idea that I had when I played with the stubs is a better way to inject stubs. Right now, when defining stubs, I have to define them directly in the Resource. While this makes it great to read, it limits me in defining different stubs per test. Let's say, for one test I want to return a list with 4 entries, while for another test I want to return a list with zero entries.

Right now I am solving this by writing my own StubbingController where I register my stubs for routes. This, however, makes it annoying to define my Resouces, because I have to repeat myself a lot. A current Resource in my project looks like this:

public struct API {
    @Injected(\.apiClient) private static var apiClient: APIClient
    @Injected(\.stubbingController) private static var stubbingController: StubbingController
    
    public static func login(username: String, password: String) -> Resource<Credentials> {
        return Resource(
            apiClient: apiClient,
            method: .post,
            path: "/auth/login",
            body: .encodable([
                "username": username,
                "password": password
            ]),
            stub: stubbingController.resolveStub(
                for: .post,
                path: "/auth/login"
            )
        )
    }
}

As you can see, I can now dynamically resolve my stubs, however, I have to repeat this code for all my endpoints. If a mechanism like this was pre-built into Fetch, I would save myself a lot of work and the project would gain more flexibility.

I would love more opinions on these two ideas, especially since one of them is potentially API-breaking.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions