Skip to content

Comments

Refactor testing utilities#56

Open
aryan-25 wants to merge 12 commits intoswift-server:mainfrom
aryan-25:refactor-testing-utils
Open

Refactor testing utilities#56
aryan-25 wants to merge 12 commits intoswift-server:mainfrom
aryan-25:refactor-testing-utils

Conversation

@aryan-25
Copy link
Collaborator

Motivation:

We currently have two mechanisms for testing NIOHTTPServer in an end-to-end manner. The first mechanism involves starting up NIOHTTPServer on localhost, and setting up a NIO client (backed by NIO's ClientBootstrap) to send requests / observe responses. The second mechanism involves spoofing a NIOAsyncTestingChannel as the channel NIOHTTPServer runs on, wiring a client channel (also backed by a NIOAsyncTestingChannel) to the server channel, and writing/reading to/from the client channel.

Currently, both mechanisms share some duplicated code, have no documentation, and in general are poorly named/structured. We should improve this.

Modifications:

  • Functionality of the existing Client type broken down into NIOHTTP1Client and NIOSecureUpgradeClient.
    • NIOSecureUpgradeClient's setUpChannel method now returns a NegotiatedClientConnection type instead of just opening one stream and returning the stream channel. For HTTP/2, NegotiatedClientConnection has an openStream method; this enables multiple streams to be created in tests.
  • The NIOAsyncTestingChannel-based mechanism is now structured into TestingChannelHTTP1Server and TestingChannelSecureUpgradeServer. Both contain a withClient static function that vend a TestingChannelHTTP1Client/TestingChannelSecureUpgradeClient respectively.
  • Added documentation for all test utilities and some general refactoring.

Result:

Easier to write more involving tests.

@aryan-25 aryan-25 added the semver/none No version bump required. label Feb 12, 2026
@gjcairo gjcairo self-requested a review February 12, 2026 11:21
@gjcairo gjcairo removed their assignment Feb 12, 2026
/// With the ``NIOAsyncChannel`` the `setUpClient` methods vend, one can write `HTTPRequestPart`s to the channel
/// and observe `HTTPResponsePart`s from the inbound stream of the async channel.
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
struct NIOHTTP1Client {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it a bit weird that we have only static methods on this type.
I think we should either change it to an actual wrapper of a channel (and initialise it accordingly in the init), or perhaps extend Channel and ClientBootstrap instead with these methods (and rename them to something like setUpTestHTTP1ClientHandlers() and setUpTestHTTP1Client(at:)).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've managed to structure these methods under Channel and ClientBootstrap now.
The end result is that NIOHTTP1Client (and NIOSecureUpgradeClient) are no longer required.

From tests, we can now simply just do this:

let client = try await ClientBootstrap(group: .singletonMultiThreadedEventLoopGroup)
                    .connectToTestHTTP1Server(at: serverAddress)


@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
/// Provides a HTTP client with ALPN negotiation.
struct NIOSecureUpgradeClient {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar comments to the H1 client.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extension NIOAsyncTestingChannel {
/// Forwards all of our outbound writes to `other` and vice-versa.
func glueTo(_ other: NIOAsyncTestingChannel) async throws {
await withThrowingTaskGroup { group in
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be withThrowingDiscardingTaskGroup

@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
extension TestingChannelHTTP1Server {
/// A plaintext HTTP/1.1 client backed by a `NIOAsyncTestingChannel`.
struct Client {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's a bit weird we have a client nested inside a server. Can't this live in the NIOHTTP1Client?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following from #56 (comment), I've removed the dedicated Client type here.

I've added a simpler withConnectedClient method in TestingChannelHTTP1Server that sets up a NIOAsyncTestingChannel and calls the new configureTestHTTP1ClientPipeline() method (under Channel) on it.

@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
extension TestingChannelSecureUpgradeServer {
// A Secure Upgrade HTTP client backed by a `NIOAsyncTestingChannel`.
struct Client {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment here as in the H1 client

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

).get()

let connectionPromise = clientTestChannel.eventLoop.makePromise(of: Void.self)
clientTestChannel.connect(to: try .init(ipAddress: "127.0.0.1", port: 8000), promise: connectionPromise)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this port matter at all or is it needed just to activate the channel?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's needed just to activate the channel. I've changed this to call the createActiveChannel() method instead, which documents the need for calling connect().

@aryan-25 aryan-25 requested a review from gjcairo February 20, 2026 14:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

semver/none No version bump required.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants