@crazydos/nuxt-msw
v1.0.2
Published
Nuxt module integration for MSW (Mock Service Worker)
Downloads
1,289
Readme
nuxt-msw
nuxt-msw
integrates MSW (Mock Service Worker) into a Nuxt project, allowing you to use it for API mocking during development. Most of the code can be directly shared with test mocks.
Features
- 🌲 Write once, use everywhere
- 🚀 Use MSW powerful mocking features in Nuxt development
- ⛰ Intercept both server-side and client-side request, including
$fetch
,useFetch
and any other api requests. - 🥧 Support Nuxt layer.
Online Playground
You can try out the functionality of nuxt-msw
online through the following website.
Migrate: 0.x
-> 1.x
users
Thank you to all the users of version 0.x. I believe you won’t need to put in too much effort to upgrade to version 1.x.
The differences between 0.x and 1.x are as follows:
optionPath
innuxt.config
has been changed tofolderPath
. Originally, it pointed to a file, but now it points to a folder.- You need init MSW worker file manually.
nuxt-msw
no longer auto provide a worker file. - The settings for worker and server were previously written in the same file. To better utilize the environments they run in, worker and server will now run in separate files. Please refer to the instructions below.
- Added support for unit testing in the Nuxt environment.
- Added support for Nuxt layers.
If you don't want upgrade to 1.x now, you can find the 0.x doc here: 0.2.x doc
Setup
To install the module to your Nuxt application:
npm install @crazydos/nuxt-msw msw --save-dev
export default defineNuxtConfig({
modules: ['@crazydos/nuxt-msw'],
msw : {
// Options, see below
}
})
Usage
You need to set up the MSW worker and server separately. When either one is set up, that side will start running.
The setup location is in the ~/msw
directory (by default), and you configure it through the corresponding files.
worker
To set up the worker, you need follow the MSW documentation to create a worker file.
# For example
npx msw init public --save
Next, you need to create a worker.{ts|js|mjs|cjs}
file in the ~/msw
directory. The worker file will be run in the Nuxt client plugin, which in browser context. Means you can use browser api and Nuxt composable in this file.
// ~/msw/worker.ts
import { http, HttpResponse } from 'msw'
export default defineNuxtMswWorkerOption(() => {
const handlers = [
// Intercept "GET /api/user" requests...
http.get('/api/user', () => {
// ...and respond to them using this JSON response.
return HttpResponse.json({
message: "Hello Worker!",
})
}),
]
// You can access any browser api
// window.location.href
return {
handlers,
workerOptions: {
// ...you can pass options to worker.start()
// onUnhandledRequest: 'bypass',
},
onWorkerStarted(worker, nuxtApp) {
// Module will setup worker when nuxt run client plugin
// Means this function will be called after plugin call worker.start()
nuxtApp.hook('app:mounted', () => {
// const route = useRoute()
// console.log(worker.listHandlers())
})
},
}
})
You can now try to fetch the data from the worker.
<script setup>
onMounted(async () => {
const res = await $fetch("/api/user")
console.log(res) // { message: "Hello Worker!" }
})
</script>
server
The way to set up the server is similar to the worker. You need to create a server.{ts|js|mjs|cjs}
file in the ~/msw
directory.
The server file will be run in Node.js Nitro
context. Because it is before NuxtApp created, you can not access NuxtApp and composable which access it. But you can access msw server and request (h3Event).
One more important thing is that, for your mock and nitro server handler work properly, you need to set baseURL
in the server option. The baseURL must be same as your server listening address.
And, when mocking the server side request, you need include the baseURL in your handler's path.
// ~/msw/server.ts
import { http, HttpResponse } from 'msw'
export default defineNuxtMswServerOption(() => {
// assume your server listening at http://localhost:3000
const baseURL = "http://localhost:3000"
// composables that not related to NuxtApp can be used here, like: useRuntimeConfig
const handlers = [
// Intercept "GET http://localhost:3000/user" requests...
http.get(baseURL + '/api/user', () => {
// ...and respond to them using this JSON response.
return HttpResponse.json({
message: "Hello msw server!"
})
}),
]
return {
baseURL, // baseURL is required
handlers,
serverOptions: {
onUnhandledRequest: 'bypass',
},
onRequest(mswServer, h3Event) {
// This funtion will be call when Nitro server "request" hook
console.log('Hello from onRequest')
mswServer.use(/*...*/)
},
}
})
After setting up the server, you can now try to fetch the data from the server.
<template>
<h1>{{ data?.message }}</h1>
<!-- Hello msw server! -->
</template>
<script setup>
const { data, error } = await useFetch('/api/user')
</script>
The h3Event
has some basic information about the request, such as event.path
. If you need more, you can explicitly install h3
, and use it to get more.
import { getQuery } from 'h3'
// ...
{
// ....
onRequest(mswServer, h3Event) {
const query = useQuery(h3Event)
// do something with query
}
}
Module options
The option usage could be found at following.
[!NOTE]
It is important to understand that these options is build time config. So if you use environment variable like.env
to decide these options, you need setup.env
atnuxi build
too.
export default defineNuxtConfig({
modules: ['@crazydos/nuxt-msw'],
msw: {
/**
* Whether to enable the module. Default to `true`
*
* This is useful when you need to enable or disable msw based on the environment.
*
* When `enable: false`, the msw worker and server will not try to intercept requests, but keeps auto import composables for type checking.
*
* @default true
*
* @example
* {
* enable: process.env.TEST === 'true'
* }
*/
enable: true, // default: true
/**
* Path to the nuxt-msw runtime folder that include worker and server files.
* @default "~/msw"
*/
folderPath: "~/msw"
/**
* Should include nuxt layers msw settings or not
* @default true
*/
includeLayer?: boolean
/**
*
* Enable unit test mode. default: false
*
* When this mode is enabled, you can use `setupNuxtMswServer` to start the msw server in a unit test environment (which is a Node.js environment).
*
* When enable: false, this mode is inactive.
*
* @default true
*/
testUtils?: boolean
},
})
How to Use in Tests
When performing unit tests with Nuxt, you might use @nuxt/test-utils
with vitest
.
vitest
If you are only using Vitest without the @nuxt/test-utils
environment, you will need to manually set up MSW.
Vitest has an example how to use msw in unit test.
[!NOTE] In this scenario, if you have handlers set up in the Nuxt layer, you will need to export them for use.
@nuxt/test-utils
Let's talk about unit test part first.
Unit Tests
According to the Nuxt official documentation, if you need to test within the Nuxt context (to ensure composables and other features function correctly), you must set up the Nuxt environment.
To ensure that MSW works correctly in this situation, nuxt-msw provides a helper function for environment setup and configuring the MSW server.
First, you need enable testUtils
option for nuxt-msw
module. Suggest enable in vitest.config.ts
.
// vitest.config.ts
import { defineVitestConfig } from '@nuxt/test-utils/config'
export default defineVitestConfig({
// any custom Vitest config you require
test: {
// .....
environmentOptions: {
nuxt: {
domEnvionment: 'happy-dom',
host: 3000,
overrides: {
msw: {
testUtils: true,
},
},
},
},
},
})
Then, you can use setupNuxtMswServer
to setup MSW server in unit test. (Note: can't run in setup files currently. So you need to run it in every test file.)
The component may look like this:
<script setup lang="ts">
const { data } = await useFetch(
'/api/user',
{ baseURL: 'http://localhost:3000' }
)
</script>
<template>
<div id="name">{{ data.name }}</div>
</template>
// @vitest-environment nuxt
import { it, expect, afterEach } from 'vitest'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import UserName from './UserName.vue'
import { setupNuxtMswServer } from '@crazydos/nuxt-msw/test-utils'
import { http, HttpResponse } from "msw"
const mswServer = await setupNuxtMswServer({
handlers: [
http.get(
'http://localhost:3000/api/user',
() => HttpResponse.json({ name: 'msw server unit' })
)
],
serverOptions: {
// onUnhandledRequest: 'bypass',
},
})
afterEach(() => {
// you can do anything with mswServer
mswServer.resetHandlers()
})
it('displays message', async () => {
// replace handler if needed
// mswServer.use(
// http.get('/api/user', () => {
// return HttpResponse.json({
// name: 'msw server unit replaced',
// })
// })
// )
const component = await mountSuspended(UserName)
expect(component.find('#name').text()).toBe('msw server unit')
})
Then you can run the test with vitest
:
npx vitest
Note: baseURL
in unit test
If you are using $fetch
or useFetch
with no baseURL
set, and use path with no domain like /some/path
. You can mock like following:
// for example
await useFetch('/api/user')
// mock with baseURL
const baseURL = 'http://localhost:3000'
await setupNuxtMswServer({
baseURL,
handlers: [
http.get(baseURL + '/api/user', () => {
return HttpResponse.json({ name: 'msw server unit' })
})
]
})
[!NOTE]
baseURL
here is just a valid domain, it is not necessary to match nuxt server address. But if you run a real server and want msw fallback to there, you need to set baseURL to the real server address.
E2E Tests
@nuxt/test-utils
uses Playwright as the E2E test driver, and it actually runs the Nuxt server and browser in a separate process. This means we can't directly modify mock in test.
To modify mocks in this situation, you need to employ workarounds, such as changing the mocks through dynamic mock.
The way nuxt-msw runs in this situation is just like normal Nuxt application. So you can just use the same way to setup MSW server and worker.
// For example: dynamic mock
// Normal test case
test('normal mock', async ({ page, goto }) => {
await goto('/', { waitUntil: 'hydration' })
await expect(page.getByText('John')).toBeVisible()
})
// Alternative test case
test('test dynamic mock', async ({ page, goto }) => {
// The server response still the same
await goto('/?alternative=true', { waitUntil: 'hydration' })
// Next, we click a button that make a request from client side
await page.getByTestId('get-data-button').click()
// We get a different response from same api endpoints
await expect(page.getByText('Jane')).toBeVisible()
})
Note: $fetch
and fetch
in @nuxt/test-utils/e2e
@nuxt/test-utils/e2e
provide $fetch
and fetch
to make request to nuxt server in e2e test. nuxt-msw
will do nothing with these two functions becuase they are outside the Nuxt application. So use them as normal.
import { $fetch, fetch } from '@nuxt/test-utils/e2e'
const html = await $fetch('/')
Nuxt Layer
(Although I think the simplest way to share handlers is just share the handlers.) There may be some cases your teams want to share msw settings by Nuxt layer. nuxt-msw
also support this.
nuxt-msw
will merge the worker and server configurations at each level. Depending on the configuration options, they will either be merged or overwritten. Below is the behavior of each option:
// worker and server options
{
handlers: [] // merge. the higher level take precedence
workerOptions: {} // merge. the higher level take precedence
serverOptions: {} // merge. the higher level take precedence
baseURL: "" // overwrite. the higher level take precedence
onWorkerStarted: () => {} // merge. the lower level execute first
onRequest: () => {} // merge. the lower level execute first
}
Here is how nuxt.config
will be resolved:
export default defineNuxtConfig({
msw: {
includeLayer // overwrite. the higher level take precedence
enable // overwrite. the higher level take precedence
testUtils // overwrite. the higher level take precedence
folderPath // every layer follow its own folderPath setting
}
})
folderPath
is the only option that is not merged. Each layer will follow its own folderPath
setting. For example, if you set folderPath
to ./msw
but set ~/msw
at the layer user level, nuxt-msw will find msw files in following folders:
~/msw
/path-to-layer-folder/msw
nuxt layer and unit test
If you need share msw settings in layers for unit test, you can add a file named unit.{ts|js|mjs|cjs}
in the layer folder (app level also support this file). The merge strategy is same as worker and server file.
unit.{ts|js|mjs|cjs}
file will be run in Vitest environment, so you can access in this file. Acctually, a part of work of setupNuxtMswServer
in unit test is import this file and merge the options.
setupNuxtMswServer
accept the same options as defineNuxtMswTestOptions
return. The options of setupNuxtMswServer
will be merged with the options of unit.{ts|js|mjs|cjs}
file. setupNuxtMswServer
will take highest precedence.
import { http, HttpResponse } from 'msw'
export default defineNuxtMswTestOptions(() => {
const handlers = [
http.get('/api/user', () => {
// ...and respond to them using this JSON response.
return HttpResponse.json({
id: 'Mock01',
name: 'msw server unit',
})
}),
]
return {
baseURL: 'http://localhost:3000',
handlers,
serverOptions: {
// onUnhandledRequest: 'bypass',
},
}
})
// xxx.spec.ts
import { setupNuxtMswServer } from '@crazydos/nuxt-msw/test-utils'
import { http, HttpResponse } from "msw"
const mswServer = await setupNuxtMswServer(
/**
* You can still pass options to overwrite the options in unit.ts
*/
)
Contribution
# Install dependencies
pnpm install
# Generate type stubs
pnpm dev:prepare
# Develop with the playground
pnpm dev
# Build the playground
pnpm dev:build
# Run ESLint
pnpm lint
Learn more about authoring nuxt module