Injecting/overriding behavior in existing handlers (higher-order resolvers?) #2392
Replies: 3 comments
-
Update: I invested some more hours of my time and came up with Now I can generate higher-order functions as easy as never before: import { HttpResponse } from "msw";
import { buildResolverWrapper } from "msw-higher-order";
import { z } from "zod";
import { fromError } from "zod-validation-error";
export const withJsonBodyZodValidation = (schema?: z.ZodType) =>
buildResolverWrapper(async ({ request }) => {
if (!schema) return;
const body = await request.clone().json();
try {
schema.parse(body);
} catch (error) {
if (error instanceof z.ZodError) {
return HttpResponse.text(fromError(error).toString(), { status: 400 });
}
throw error;
}
});
const parameterizedHandler = (schema: z.ZodType) =>
http.post(
"/",
withJsonBodyZodValidation(schema)(() => HttpResponse.text("ok")),
);
server.use(parameterizedHandler(z.strictObject({ hello: z.string() })));
Remaining Challenges
I hope that writing down all this will help others having the same problem. I'd love to know how you solve higher-order resolvers in MSW! |
Beta Was this translation helpful? Give feedback.
-
Hi, @TimKochDev 👋 Thanks for sharing your feedback. Higher-order resolvers is definitely a thing, but there isn't any dedicated API from MSW to assist you in creating them. At this point, it's just a higher-order function with no magic. And I don't think we should change that. You are welcome to design whichever custom logic around MSW's primitives that suits your application and the testing strategy you have. MSW's goal is to give you enough powerful primitives to do that without shipping too much niche APIs. I believe we strike that balance well at the moment. Perhaps you are approaching runtime request handlers (
The purpose of using overrides is to define the truth, not so much to accumulate behaviors. In other words, when testing, say, error scenarios, it is never the point to define a handler that says "work as usual but then in this case respond with an error". The point is always to define an override handler that says "always respond with an error". Since you bind the override to a particular test, you can disregard the happy path behaviors because they are irrelevant for the error test case. This rids you of the need to have complex behavior composition for testing controlled behaviors of your API.
Which it is! I don't see anything awkward about it. MSW doesn't ship middleware intentionally to make sure the request handling is always WYSIWYG. We are rather careful with the recommendations and even the APIs we provide not to facilitate complexity in user setups. You can always get that complexity if it's needed, but it must never be encouraged. You can take advantage of how MSW resolves requests to implement the middleware logic in numerous forms. For example, the Execution order of handlers means you can prepend more specific overrides and make them passthrough if you wish to augment happy path behaviors with a test-specific validation (which is what you did with one of your approaches). You can also implement something like a http.get('/user', middleware(resolverOne, resolverTwo, resolverThree)) This will give you a more conventional request flow, allowing you to compose request handling logic. Add on top the fact that MSW short-circuits any request as soon as its resolver handles it, and you can even combine the CautionBut there's a big and important but. All of the above is nice, except it brings a ton of complexity to your testing setup. This is why I rather encourage you not to do any of that. There's nothing fundamentally wrong with repetition. By bringing these layers of setup on top of MSW, you make your mocks harder to read and debug. That's now worth the price. If you absolutely want some reusability, then at least consider resolver-level composition like this: http.get('/user', async ({ request }) => {
await utilOne(request)
await utilTwo(request)
await defaultHandling(request)
})
|
Beta Was this translation helpful? Give feedback.
-
Wow, what a detailed response! The most important thing for me to learn was that I can throw mocked responses in resolvers. That already helps a lot. See the example below which I already used in my first post:
While the code above tests the schema of the request body, I'd sometimes like to test if the exact and correct values are sent (e.g. when testing a UserForm component). Knowing that resolvers can throw responses, I will do the following in the future:
Where With this approach I don't need to "inject/override behavior in existing handlers" (the title of this discussion) anymore. Do you agree @kettanaito ? |
Beta Was this translation helpful? Give feedback.
-
I love MSW and could not imagine developing without it any more. Thank you!
However, there is one problem that always annoys me.
Problem Statement
The documentation rightfully encourages to co-locate response handlers for "happy paths" and to use custom handlers when testing edge cases. These custom handlers do not reuse anything of the existing "happy path" handlers.
Motivation
Imagin the following
MswUserHandlers.ts
:And a
UserForm.test.tsx
:This FAILS to test if the posted name of the user was "John Doe" as the handler only checks for any string.
I somehow need to inject that little
z.strictObject({ name: z.literal("John Doe") })
into the handler.I hope you see the bigger picture from this example. This is not about users and not even about validation. Another example would be if I wanted the handler to indeed update the user store and only then send an error response for whatever reason. I don't want to duplicate 90% of the handler's logic just for this little change.
Approaches
Since validation was my original motivation I came up with the following solution. This is type-safe and correctly narrows down the body type so that the wrapped resolver is aware of the body type validated by the higher-order function.
However, when looking closely, this solution does not address the right "level" of the problem. On a more abstract level, it prepends the wrapped resolver with new behavior. To me this looks like something that we can take more advantage of. My next step would be to migrate this to something like a generic function
before(newBehavior: ResponseResolverButMayReturnUndefined, wrappedResolver)
. IfnewBehavior
returnsundefined
, thenwrappedResolver
is called. However, so far I failed to get the types right.This awkwardly feels like recreating my own type-safe middleware logic...
One thing to note here: Even if I am able to build such a generic higher-order function to prepend/append logic for myself, I'd still need to write
http.post("/users", before(validation, postUserResolver))
in the test file which duplicates the HTTP method and - more importantly - the path since this information is already defined in the "happy path" handlers.Beta Was this translation helpful? Give feedback.
All reactions