๋ค์ด๊ฐ๊ธฐ
ํ๊ฒฝ
node 20.16
ํจํค์ง ์์กด์ฑ
react testing library 13.4.0
redux toolkit 1.9.5
msw 2.3
vite 5.2.11
typescript 4.9.4
jest 29.3.1
msw ํธ๋ค๋ฌ๋ก ์๋ฒ ์๋ต ๋ชจํน, RTK Query๋ก API ํธ์ถํ๋ ์ปดํฌ๋ํธ์์ ๋ฐ์ํ๋ ์๋ฌ ๋ชจ์zip...
ํ ์คํธ๋ก ์ฝ๋ ํ์ง์ ํฅ์์ํค๊ณ ์ ํ์ผ๋ jest testing framework + msw 2 ํ๊ฒฝ์์ ์ ํ ํ๋ค๊ฐ ์ ์ ๊ฑด๊ฐ์ ์ํ์ ๋ฐ์๋ค
msw ์์๋ ์๊ธด๊ฒ ์ด๋ ๊ฒ ์ ํ ํ๋๊ฒ ๊ท์ฐฎ์ผ๋ฉด ~ ๋ ๋ชจ๋ํ ํ ์คํ ํ๋ ์์ํฌ ์ฌ์ฉํ๋ ด~ ์ด๋ผ๊ณ ๊ถ์ฅ..
If you find this setup cumbersome, consider migrating to a modern testing framework, like Vitest, which has none of the Node.js globals issues and provides native ESM support out of the box.
๊ทธ์น๋ง ์ด๋ฒ์ ์ ํ ํ๋ฉด์ node์ ๋ํด์๋ ์ง์์ ํ์ฅํ ๋๋์ด๋ค.
์ด๋งํ๊ฒ ์ ํ ์ ํ์ ๋ค์๊ธฐ์ ๊ธฐ๋กํด๋ณด๋ ์๋ฌ ๋ชจ์ ์์
1. Cannot find module ‘msw/node’ (JSDOM)
2. Request/Response/TextEncoder is not defined (Jest)
TO-BE
//jest.polyfills.js
// jest.polyfills.js
/**
* @note The block below contains polyfills for Node.js globals
* required for Jest to function when running JSDOM tests.
* These HAVE to be require's and HAVE to be in this exact
* order, since "undici" depends on the "TextEncoder" global API.
*
* Consider migrating to a more modern test runner if
* you don't want to deal with this.
*/
const { TextDecoder, TextEncoder } = require('node:util')
Object.defineProperties(globalThis, {
TextDecoder: { value: TextDecoder },
TextEncoder: { value: TextEncoder },
})
const { Blob, File } = require('node:buffer')
const { fetch, Headers, FormData, Request, Response } = require('undici')
Object.defineProperties(globalThis, {
fetch: { value: fetch, writable: true },
Blob: { value: Blob },
File: { value: File },
Headers: { value: Headers },
FormData: { value: FormData },
Request: { value: Request },
Response: { value: Response },
})
//jest.config.js
// jest.config.js
module.exports = {
setupFiles: ['./jest.polyfills.js'],
}
ํด๋น ๋ฌธ์๋๋ก fetch๋ฅผ polyfillํด์ฃผ๋ `undici` ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค์น๋ ์๋ฃ!
3. ReferenceError: require is not defined
ํ์ง๋ง ์์์ฒ๋ผ ๊ณต์๋ฌธ์๋๋ก ๊ทธ๋๋ก ์ ์ด์ค `jest.polyfills.js` ํ์ผ์์ ์๋ฌ ๋ฐ์
stackover flow์์ ์ฐธ๊ณ ํ์ฌ .cjs ํ์ผ๋ก ๋ณ๊ฒฝ
AS-IS
//jest.polyfills.js
//package.json ์ ์๋ ๋์ด์๋ ์ค์
{
"type": "module"
}
module๋ก ๋์ด์์ผ๋ฏ๋ก ECMAScript module(ESM)์ ๋ฐ๋ผ `.js` ํ์ผ์ ESM์ผ๋ก ํ์ฑ๋๋ค.
module ๋ชจ๋์์ CommonJS๋ฅผ import ํด์ค๋ ค๋ฉด `.cjs` ํ์ฅ์๋ก ๋ณ๊ฒฝํ๋ฉด ๋๋ค.
TO-BE
//jest.polyfills.cjs
eslint ์ค๋ฅ๊ฐ ๋๋๋ฐ intellisense๋ก /* eslint-disable import/order */ ๋ฅผ ๋งจ ์์ค์ ์ ์ด์ค๋ค.
4. ReferenceError: ReadableStream is not defined, ReferenceError: TransformStream is not defined
TO-BE
//jest.polyfills.cjs
const { ReadableStream, TransformStream } = require('node:stream/web'); //<--
Object.defineProperties(globalThis, {
ReadableStream: { value: ReadableStream }, //<--
TransformStream: { value: TransformStream }, //<--
})
์ฃผ์!
ReadableStream์ require('node:util') ๋ชจ๋์ด ์๋ require('node:stream/web') ๋ชจ๋์์ ๊ฐ์ ธ์ค๋๋ก ์ฃผ์!
์ฒ์์ node:util๋ก ์ ์ด๋๋ค๊ฐ ๋์ค์ ์ฌ๊ธฐ์ ๋ ์๋ฌ๊ฐ ๋ฌ๋ค.
5. ReferenceError: clearImmediate is not defined
TO-BE
//jest.polyfills.cjs
const { TextDecoder, TextEncoder } = require('node:util')
const { clearImmediate } = require('node:timers') //<--
Object.defineProperties(globalThis, {
TextDecoder: { value: TextDecoder },
TextEncoder: { value: TextEncoder },
clearImmediate: {value: clearImmediate}, //<--
})
6. TypeError: markResourceTiming is not a function
TO-BE
//jest.polyfills.cjs
const { performance } = require("node:perf_hooks"); //<--
const { TextDecoder, TextEncoder } = require('node:util')
const { ReadableStream, TransformStream } = require('node:stream/web');
const {clearImmediate} = require('node:timers')
Object.defineProperties(globalThis, {
TextDecoder: { value: TextDecoder },
TextEncoder: { value: TextEncoder },
ReadableStream: { value: ReadableStream },
TransformStream: { value: TransformStream },
performance: { value: performance }, //<--
clearImmediate: {value: clearImmediate},
})
7. intercepted a request without a matching request handler ๋๋ฒ๊น ๊ณผ์
ํ ์คํธํ๋ Email ํ์ด์ง์ msw ํธ๋ค๋ฌ์ ์๋ฒ ์๋ต์ ๋ฐ์์ค๋ ์ฝ๋๊ฐ ์๋๋ฐ, ์๋ฌด๋ฆฌ ํ ์คํธ๋ฅผ ๋๋ ค๋ ๊ณ์ ํ ์คํธ๊ฐ ์คํจํ๋ค.
์๋ฒ์์ ์๋ต์ ๋ฐ์์จ ๊ฒฐ๊ณผ์ ๋ฐ๋ผ ํ๋ฉด์ ๋์ ์ผ๋ก ํ์ํด์ผํ๋ ํ ์คํธ๋ฅผ ํ ์คํธํ๋๋ฐ ๊ณ์ ์์๋ฅผ ๋ชป์ฐพ๋๋ค๋ ์๋ฌ๋ฐ์
Unable to find an element with the text: /์ด๋ฏธ ์ฌ์ฉ ์ค/. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
//Email.tsx
const [
checkEmailDuplicationQuery,
{ isLoading: isLoadingCheckEmailDuplication },
] = useLazyCheckEmailDuplicationQuery(); //rtk query
const [serverErrorMsg, setServerErrorMsg] = useState('');
const handleClickVerifyEmail = () => {
checkEmailDuplicationQuery(emailInputValue)
.then((res) => {
console.log('API response:', res);
if (res.data === 'true') {
showEmailInputErrorMsgWithFocus();
setServerErrorMsg(
'์ด๋ฏธ ์ฌ์ฉ ์ค์ธ ์ด๋ฉ์ผ์ด์์. ๋ค๋ฅธ ์ด๋ฉ์ผ๋ก ์๋ํด ์ฃผ์ธ์.',
);
}
.catch((err) => console.log(err));
};
์๋์ฒ๋ผ ํด๋น request๋ฅผ ์ฝ์๋ก ์ฐ์ผ๋ฉด ์ ์ฐํ๋ค.
//Email.test.tsx
it('์ด๋ฏธ ์ฌ์ฉ์ค์ธ ์ด๋ฉ์ผ์ด๋ฉด ๋ค๋ฅธ ์ด๋ฉ์ผ๋ก ์๋ํ๋ผ๋ ์๋ฌ๋ฉ์์ง ํ์ถ๋๋ค', async () => {
const { user } = renderEmail();
server.events.on('request:start', ({ request }) => {
console.log('MSW intercepted: ์ฝ์', request.method, request.url);
});
});
๊ทธ๋ฐ๋ฐ ์๋์ ๊ฐ์ด ํ ์คํธ ์ฝ๋๋ฅผ ์ถ๊ฐํ๋ฉด ๊ณ์ ์คํจํ๊ณ ์ฝ์์๋ intercepted a request without a matching request handler
์๊พธ ํธ๋ค๋ฌ์ ์ผ์นํ๋ ์์ฒญ์ด ์๋ค๊ณ ๋์จ๋ค.
//Email.test.tsx
it('์ด๋ฏธ ์ฌ์ฉ์ค์ธ ์ด๋ฉ์ผ์ด๋ฉด ๋ค๋ฅธ ์ด๋ฉ์ผ๋ก ์๋ํ๋ผ๋ ์๋ฌ๋ฉ์์ง ํ์ถ๋๋ค', async () => {
const { user } = renderEmail();
const emailInput = screen.getByLabelText('์ด๋ฉ์ผ');
await user.type(emailInput, 'test@test.com');
const confirmBtn = screen.getByRole('button', { name: 'ํ์ธ' });
await user.click(confirmBtn);
expect(await screen.findByText(/์ด๋ฏธ ์ฌ์ฉ ์ค/)).toBeInTheDocument();
});
//emailHandler.ts
//ํ์๊ฐ์
์์ฒญ์ ์ด๋ฉ์ผ์ฃผ์ ์ค๋ณต ์ฌ๋ถ
http.get('/sign-ups/email-duplications', ({ request }) => {
console.log('Mock handler called');
}
- ์ปดํฌ๋ํธ ๊ตฌํ๋ถ์์ console๋ก ๋๋ฒ๊น ํด๋ณด๋, api ์๋ต์ด ์ฑ๊ณตํ์ง ์๊ณ ์๋ฌ๋ก FETCH_ERROR ์ํ๊ฐ ๋ค์ด์ค๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
- handler๊ฐ ์คํ๋๋์ง ์ฝ์ ์ฐ์์ผ๋ ํ ์คํธ ํฐ๋ฏธ๋์์ ์ฐํ์ง ์๋๋ค. = handler ํธ์ถ ์๋จ
์ ๋ฌธ์ ๋ RTK Query์ API ์์ฒญ์ด intercepted ๋์ง ์๊ณ ์๋ ๊ฒ์ด๋ค.
msw ๋ฌธ์ RTK Query requests are not intercepted ์์ `baseUrl`์ ์ค์ ํ๋ผ๊ณ ํ๋ค.
๊ณต์๋ฌธ์์์ ์ด์๋ก ๊ฑธ์ด์ค redux toolkit์ maintainer์ ๋ต๋ณ์ ๋ณด๋, ๋ ธ๋๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๋๋ฉ์ธ์ด ์์ด์ ์๋ ๊ฒฝ๋ก๋ก ์์ฒญํ๋ฉด ๋ ธ๋์์๋ ์๋ํ์ง ์๋๋ค๊ณ ํ๋ค.
๊ทธ๋์ ๋ธ๋ผ์ฐ์ ์์ ํ ์คํธ๋ฅผ ๋๋ฆฌ๋๊ฒ ์๋๋ผ๋ฉด full url์ด ํญ์ ํ์ํ๋ค.
์ฆ, RTK Query๋ฅผ ์ฌ์ฉํด์ API๋ฅผ ํธ์ถํ ๋ ์์ฒญ์ ํ ์คํธ ํ๊ฒฝ์์ MSW v2๊ฐ intercept ํ๋ ค๋ฉด msw์ ํธ๋ค๋ฌ๋ฅผ ์ ๋ ๊ฒฝ๋ก๋ก ์ ์ด์ฃผ์ด์ผ ํ๋ค!
๋์ผ๋ก ํ์ธ์ ์ํด document.baseURI ๋ฅผ ์ฐ์ด๋ณด๋ฉด, ์๋์ ๊ฐ์ด `localhost/` ๊น์ง๋ง ๋์จ๋ค.
//setupTests.ts
TO-BE
// Set `document.baseURI` for MSW v2
Object.defineProperty(document, 'baseURI', {
value: 'http://localhost:5173',
writable: true,
});
์ ์ฝ๋์ ๊ฐ์ด baseURI๋ฅผ ์ค์ ํ๊ณ , ์ฝ์๋ก ์ฐ์ด๋ณด๋ฉด ์ด์ ๊ธฐ๋ณธ vite ํ๊ฒฝ๋ณ์๋ก ์ค์ ํด๋ `localhost:5173`์ผ๋ก ์ ์ฐํ๋ ๊ฑธ ๋ณผ ์ ์๋ค.
8. TypeError: Right-hand side of 'instanceof' is not an object
์ด์ ํ ์คํธ๋ฅผ ๋ค์ ๋๋ ค๋ณด๋, ๋๋์ด mock handler๋ ์คํ๋๋, ๋ ์๋ฌ ๋ฐ์
ReadableStream์ node:util ๋ชจ๋์ด ์๋ node:stream/web์์ ๊ฐ์ ธ์ค๋ฉด ํด๊ฒฐ๋๋ค.
์ฒ์์ ReferenceError: ReadableStream is not defined ์๋ฌ ํด๊ฒฐํ ๋ ์ด์ ์ค๋ ๋ ์ ์๋ณด๊ณ ๋งจ ์์์๋ ์ฝ๋์๋ node:util์์ ๊ฐ์ ธ์๋๋ ์ด๋ฐ right-hand ์ด์ฉ๊ณ ์๋ฌ๊ฐ ๋๋ ๊ฒ์ด์๋ค.
//jest.polyfills.cjs
AS-IS
const { ReadableStream } = require('node:util') //X
Object.defineProperties(globalThis, {
ReadableStream: { value: ReadableStream }, //<--
})
TO-BE
const { ReadableStream } = require('node:stream/web');
์ด๋ ๊ฒ ์ ํ ํ๋ฉด ํ ์คํธ ํต๊ณผ์ด๋ค!
'React' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
jest 3rd party library mocking (0) | 2024.08.29 |
---|---|
jest error: process() or/and processAsync() method of code transformer found at fileTranformer.cjs (0) | 2024.08.09 |
React Testing Library test React Router - BrowserRouter vs MemoryRouter (0) | 2024.07.30 |
vite stage ๊ฐ๋ฐ/๋ฐฐํฌ ๋ชจ๋ cli ์ค์ (0) | 2024.07.17 |
React testing - What to test, getBy vs queryBy, fireEvent vs userEvent (0) | 2024.06.20 |