New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: "setupRemoteServer" API #1617
base: main
Are you sure you want to change the base?
Conversation
51e3148
to
f1ff76a
Compare
ed70542
to
0e8d362
Compare
Interesting approach with runtimes and interceptors. I am worried that maintaining so many runtimes is already complex for users to understand, and will only grow more complex as frameworks develop. In order to understand how to mock a request in Next.js or Remix, a user has to understand where the request is being issued (client or server), what process that code is running in (is it a separate node process, is it connected to my test runner somehow?), and what runtime it is using (browser, Nodejs or "edge" like Next.js middleware). I wonder if a unified mock server could simplify the mental model for writing tests. Something like the http-middleware from v1. Drawbacks:
An alternative would be to try and hide these runtime and communication details from users more, possibly with adapters for each framework. Eager to dive into the code and contribute. Great work as always! |
I believe Artem's intention is for handlers to be interchangeable between client and server environments. You should be able to assign handlers to a constant and call it with both |
To be clear, this feature introduces no additional runtimes. The inter-process interception is still meant for Node.js but you can, technically, combine tests in Node and runtime in something like React Native and that should still work. You only need MSW in your development runtime. And that is still JavaScript, which is either the browser or Node, so in this regard nothing changes, even if you're developing edge functions or Cloudflare Workers, etc. You are still developing/testing them in Node.js.
The user doesn't have to know any of that. Request handlers remain environment-agnostic, the only thing that changes is this:
This feature doesn't discard previously existing practices, it simply expands the capabilities of your tests for better server-side network control.
Standalone HTTP server for mocking has a number of disadvantages and I would only recommend it in particular scenarios. That's why you still have This feature unifies the mocking experience even more, because if you take a look at its implementation, you can notice how very little changes here. MSW is a very simple tool. It needs a source of network to get requests, and it resolves those requests against the list of request handlers. It's been like that 5 years ago, and it's still like that today. What changes is that the new
I appreciate your advice but time has taught me that this is a slippery slope. What may seem like a better direction often leads to indirect complexity leaking to the end user. What I tend to do instead, is base my decisions on the established principles of MSW. Those served me well, they still do today and will continue so after this feature is merged. I don't want folks to configure adapters or runtimes. That's completely unnecessary and MSW has proven it so. But that's the beauty in its API design: you still can have all that if you want. You can abstract to whichever degree you feel comfortable, which includes designing your configurations, publishing your setup to the internal package registry for other teams, making it HTTP server-first, etc. Support strong defaults, allow opinionated customization. That's the key.
Thanks! I will certainly need some help with testing this once the implementation is ready. Hope to give it more attention later this/next month. |
@nickmccurdy is completely correct. All Setup 1: Test-driven network
This way the test controls the server-side traffic of the tested application entirely. Setup 2: Mixed network
This is what you'd call a traditional testing setup for Node.js right now. It's still applicable to this inter-process testing story in the same shape. Setup 3: App-driven network
This allows to strategically turn on/off the network in your app for whichever requests you wish, while the tests (or any other process, like a script) can monitor the outgoing traffic. |
I think the remote style of connection must be an explicit choice by providing the // app/entry.server.js
server.listen({
// By doing this, I tell MSW that I expect this application's network
// to be controlled by the remote instance of MSW (e.g. in my tests).
remote: true
}) This way we'd save a bit on roundtrips of having to wait for the remote request handler if the This is also where we may introduce some identity checks. It'd be a good idea to allow the developer to specify what exact process to connect to. Granted, that should be controlled on the |
|
Update the
|
I think one of the tests fails because the test wasn't updated to pass the port value to the Separately, the very long repeated logs can be solved (iirc) by testing for socket requests and passing the results through (#2041 has changes to |
Intention
Introduce an API that allows one process to modify the traffic of another process. The most apparent application for this is testing server-side behaviors of a JavaScript application:
This API is designed exclusively for use cases when the request-issuing process and the request-resolving process (i.e. where you run MSW) are two different processes.
Proposed API
With consideration to the existing MSW user experience, I suggest we add a
setupRemoteServer()
API that implements theSetupApi
interface and has a similar API tosetupServer
. The main user-facing distinction here is thatsetupRemoteServer
is affecting a remote process, as indicated by the name.The
.listen()
and.close()
methods of the remote server become async since they now establish and terminate an internal server instance respectively.You can then operate with the
remote
server as you would with a regularsetupServer
, keeping in mind that it doesn't affect the current process (your test) but instead, any remote process that runssetupServer
(your app).By fully extending the
SetupApi
, thesetupRemoteServer
API provides the user with full network-managing capabilities. This includes defining initial and runtime request handlers, as well as observing the outgoing traffic of a remote process using the Life-cycle API (remote.events.on(event, listener)
). I think this is a nice familiarity that also provides the user with more power when it comes to controlling the network.Implementation
I've considered multiple ways of implementing this feature. Listing them below.
(Chosen) WebSocket server
The
setupRemoteServer
API can establish an internal WebSocket server that can route the outgoing traffic from any server-side MSW instance anywhere and deliver it to the remote server to potentially resolve.Technically, the WebSocket server acts as a resolution point (i.e. your handlers) while the remote MSW process acts as a request supplier (similar to how the Service Worker acts in the browser).
Very roughly, this implies that the regular
setupServer
instances now have a fixed request handler that tries to check if any outgoing request is potentially handled by an existing remote WebSocket server:If no WebSocket server was found or establishing a connection with it fails within a sensible timeout period (~500ms), the
setupServer
instance of the app continues to operate as normal.IPC
The test process and the app process can utilize IPC (interprocess communication) to implement a messaging protocol. Using that protocol, the app can signal back any outgoing requests and the test can try resolving them against the request handlers you defined immediately in the test.
This approach is similar to the WebSocket approach above with the exception that it relies on IPC instead of a standalone running server. With that, it also gains its biggest disadvantage: the app process must be a child process of the test process. This is not easy to guarantee. Depending on the framework's internal implementation, the user may not achieve this parent/child relationship, and the IPC implementation will not work.
Given such a demanding requirement, I've decided not to use this implementation.
Limitations
useRemoteServer()
affects the network resolution for the entire app. This means that you cannot have multiple tests that override request handlers for the same app at the same time. I think this is more than reasonable since you know you're running 1 app instance that can only behave in a single way at a single point in time. Still, I expect users to be confused when they parallelize their E2E tests and suddenly see some network behaviors leaking across the test cases.Concerns
setupRemoteServer
only affects the server-side network behavior of any running application process with the server-side MSW integration? To affect the client-side network behavior from a test you have to 1) havesetupWorker
integration in the app; 2) set a globalwindow.worker
instance; 3) usewindow.worker.use()
to add runtime request handlers. This stays as it is right now, no changes here.The API is TBD and is subjected to change.
Roadmap
rest.all()
handler.response
insetupServer
.ReadableStream
from the remote request handler (may consider transferringReadableStream
over the WS messages instead ofArrayBuffer
, if that's allowed).ReadableStream
transfer over WebSockets that would be great.use()
test (may have something to do with the handlers management refactoring as a part of theserver.boundary()
).Blockers
socket.io-parser
are broken for the CJS build).