New API to define handlers by object syntax to reuse handlers, like CSF3 #1541
Replies: 1 comment 2 replies
-
Hey, @KoichiKiyokawa. Thanks for drafting this proposal! Let me start by addressing some of your concerns from the motivation section.
I think the difficulty here arises from the fact that these are two handlers controlling two separate resources. The mocking logic itself (the response status code, headers, body) may look repetitive but you're defining two different responses, and it's important to keep that in mind. What you really want to reuse here is the dataset. We have a library called Data that pushes the source of truth to abstract data models and manages handlers for you, if you so wish. But you can also go with a manual approach here, defining your data and then reusing it those two handlers: const users = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Kate' }
]
export const handlers = [
rest.get('/user/:id', (req, res, ctx) => {
const user = users.find(user => user.id === req.params.id)
if (!user) {
return res(ctx.status(404))
}
return res(ctx.json(user))
}),
rest.get('/users', (req, res, ctx) => {
return res(ctx.json(users))
})
] Notice how little there is actually to reuse across these handlers once you've moved the data outside.
Consider using the Life-cycle API to spy on request/response events. You can attach listeners at any point, even within your test, so they stand separated from your request handlers. // some.test.js
import { handlers } from './handlers'
const server = setupServer(...handlers)
// ...the regular setup here
it('some test', () => {
const requestListener = jest.fn()
server.on('request:start', requestListener)
// ...make your request
expect(requestListener).toHaveBeenCalledTimes(1)
expect(requestListener).toHaveBeenCalledWith(expect.objectContaining({
method: 'GET',
url: new URL('https://example.com/resource')
})
}) Note that request/response assertions are generally discouraged and I recommend writing your tests around the data/state that requests affect rather than spying on them directly. I write about it at length in here. Now to the proposal. If I understand it correctly, you suggest supporting an object-based declaration syntax for the request handlers. Your main goal here is to be able to compose handlers, so to derive new handlers from the old ones, opening more room for the reusability of things like delays, headers, paths, etc. Interestingly enough, MSW supports exactly what you're proposing with the exception of using a functional API to achieve that. import { createResponseComposition, context } from 'msw'
const baseResolver = (resolver) => {
return (req, res, ctx) => {
const customResponseFn = createResponseComposition(null, [
// Delay all responses by 500ms by default.
// You can define any "ctx.*" methods here just as you do
// when calling the "res()" function by default.
context.delay(500)
])
// Notice that we're passing the custom "res" function here
// that has the default delay built-in.
return resolver(req, customResponseFn, ctx)
}
}
const userResolver = baseResolver((req, res, ctx) => {
// The response will be delayed based on the base resolver.
return res(ctx.json(singleUser))
})
const usersResolver = baseResolver((req, res, ctx) => {
return res(ctx.json(allUsers))
})
export const handlers = [
rest.get('/user/:id', userResolver),
rest.get('/users', usersResolver)
] Notice that the data returned from the My main concern with the syntax you propose is that handlers stop being as deterministic. Since a handler may depend on a bunch of things from another handler (keep in mind, implicitly), it becomes really hard to understand and reason about your network behavior. This is especially true if you rely on things like There's a great advantage in keeping request handlers plain. Bear in mind, that doesn't mean repetitive. You can reuse a fair share of logic but you must keep the resource path/resolver contract explicit and clear. I don't think we will adopt a proposed API any time soon. I am always open to making MSW APIs simpler and plainer, and I find your proposal bringing a bit of complication to how handlers are defined. Although it may seem like you're saving yourself some effort by reusing things, in actuality you'd be causing more harm than meets the eye. I'm thankful for the proposal regardless. Feel free to elaborate below, maybe we can come to a different conclusion or a compromise after giving this a due discussion. Thanks! |
Beta Was this translation helpful? Give feedback.
-
Scope
Adds a new behavior
Compatibility
Feature description
First, thank you for all of your work on this library 🎉
Motivation
There are cases where one handler wants to be reused by another handler.
For example,
Even though these two handlers are mostly similar, it is difficult to reuse
userShowHandler
when we writeuserListHandler
.Also, it is difficult to inject
spy method
to existing handlers in test.Proposed Solution
Use object to define handler.
I was inspired by CSF3 (https://storybook.js.org/blog/component-story-format-3-0/), which is a reusable story definition for storybook and others.
I created a POC library to implement this syntax.
https://github.com/KoichiKiyokawa/msw-object
Beta Was this translation helpful? Give feedback.
All reactions