import * as React from 'react'import * as api from './api'function Course({ courseId }) { const [state, setState] = React.useState({ loading: false, course: null, error: null, }) const { loading, course, error } = state React.useEffect(() => { setState({ loading: true, course: null, error: null }) api.getCourseInfo(courseId).then( (data) => setState({ loading: false, course: data, error: null }), (e) => setState({ loading: false, course: null, error: e }), ) }, [courseId]) return ( <> <div role="alert" aria-live="polite"> {loading ? 'Loading...' : error ? error.message : null} </div> {course ? <CourseInfo course={course} /> : null} </> )}function CourseInfo({ course }) { const { title, subtitle, topics } = course return ( <div> <h1>{title}</h1> <strong>{subtitle}</strong> <ul> {topics.map((t) => ( <li key={t}>{t}</li> ))} </ul> </div> )}export default Courseapi.getCourseInfo(courseId) 호출을 모킹(Mocking)할 것입니다. 이 컴포넌트가 수행하는 작업에 대해 검증(Assert)해야 할 몇 가지 사항은 다음과 같습니다.// 🛑 이렇게 하지 마세요...import * as React from 'react'import { render, wait, cleanup } from '@testing-library/react/pure'import { getCourseInfo } from '../api'import Course from '../course'jest.mock('../api')function buildCourse(overrides) { return { title: 'TEST_COURSE_TITLE', subtitle: 'TEST_COURSE_SUBTITLE', topics: ['TEST_COURSE_TOPIC'], ...overrides, }}describe('Course success', () => { const courseId = '123' const title = 'My Awesome Course' const subtitle = 'Learn super cool things' const topics = ['topic 1', 'topic 2'] let utils beforeAll(() => { getCourseInfo.mockResolvedValueOnce( buildCourse({ title, subtitle, topics }), ) }) afterAll(() => { cleanup() jest.resetAllMocks() }) it('should show a loading spinner', () => { utils = render(<Course courseId={courseId} />) expect(utils.getByRole('alert')).toHaveTextContent(/loading/i) }) it('should call the getCourseInfo function properly', () => { expect(getCourseInfo).toHaveBeenCalledWith(courseId) }) it('should render the title', async () => { expect(await utils.findByRole('heading')).toHaveTextContent(title) }) it('should render the subtitle', () => { expect(utils.getByText(subtitle)).toBeInTheDocument() }) it('should render the list of topics', () => { const topicElsText = utils .getAllByRole('listitem') .map((el) => el.textContent) expect(topicElsText).toEqual(topics) })})describe('Course failure', () => { const courseId = '321' const message = 'TEST_ERROR_MESSAGE' let utils, alert beforeAll(() => { getCourseInfo.mockRejectedValueOnce({ message }) }) afterAll(() => { cleanup() jest.resetAllMocks() }) it('should show a loading spinner', () => { utils = render(<Course courseId={courseId} />) alert = utils.getByRole('alert') expect(alert).toHaveTextContent(/loading/i) }) it('should call the getCourseInfo function properly', () => { expect(getCourseInfo).toHaveBeenCalledWith(courseId) }) it('should render the error message', async () => { await wait(() => expect(alert).toHaveTextContent(message)) })})act 경고가 발생할 수 있습니다. (이 특정 예시의 경우)처음 두 가지 포인트는 무엇을 테스트하든 상관없이 적용 가능하다는 점에 주목할 필요가 있습니다. 세 번째는 Jest와 act 사이의 구현 세부 사항(Implementation detail)에 가깝습니다.// ✅ 이렇게 하세요import { render, screen, wait } from '@testing-library/react'import * as React from 'react'import { getCourseInfo } from '../api'import Course from '../course'jest.mock('../api')afterEach(() => { jest.resetAllMocks()})function buildCourse(overrides) { return { title: 'TEST_COURSE_TITLE', subtitle: 'TEST_COURSE_SUBTITLE', topics: ['TEST_COURSE_TOPIC'], ...overrides, }}test('course loads and renders the course information', async () => { const courseId = '123' const title = 'My Awesome Course' const subtitle = 'Learn super cool things' const topics = ['topic 1', 'topic 2'] getCourseInfo.mockResolvedValueOnce(buildCourse({ title, subtitle, topics })) render(<Course courseId={courseId} />) expect(getCourseInfo).toHaveBeenCalledWith(courseId) expect(getCourseInfo).toHaveBeenCalledTimes(1) const alert = screen.getByRole('alert') expect(alert).toHaveTextContent(/loading/i) const titleEl = await screen.findByRole('heading') expect(titleEl).toHaveTextContent(title) expect(screen.getByText(subtitle)).toBeInTheDocument() const topicElsText = screen .getAllByRole('listitem') .map((el) => el.textContent) expect(topicElsText).toEqual(topics)})test('an error is rendered if there is a problem getting course info', async () => { const message = 'TEST_ERROR_MESSAGE' const courseId = '321' getCourseInfo.mockRejectedValueOnce({ message }) render(<Course courseId={courseId} />) expect(getCourseInfo).toHaveBeenCalledWith(courseId) expect(getCourseInfo).toHaveBeenCalledTimes(1) const alert = screen.getByRole('alert') expect(alert).toHaveTextContent(/loading/i) await wait(() => expect(alert).toHaveTextContent(message))})act 경고를 더 이상 받지 않게 될 것입니다.FAIL src/__tests__/course-better.js ● course loads and renders the course information Unable to find an element with the text: Learn super cool things. 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. <body> <div> <div aria-live="polite" role="alert" /> <div> <h1> My Awesome Course </h1> <ul> <li> topic 1 </li> <li> topic 2 </li> </ul> </div> </div> </body> 40 | expect(titleEl).toHaveTextContent(title) 41 | > 42 | expect(getByText(subtitle)).toBeInTheDocument() | ^ 43 | 44 | const topicElsText = getAllByRole('listitem').map(el => el.textContent) 45 | expect(topicElsText).toEqual(topics) at getElementError (node_modules/@testing-library/dom/dist/query-helpers.js:22:10) at node_modules/@testing-library/dom/dist/query-helpers.js:76:13 at node_modules/@testing-library/dom/dist/query-helpers.js:59:17 at Object.getByText (src/__tests__/course-better.js:42:10)수동 테스터를 위한 테스트 케이스 워크플로우를 생각하고, 각 테스트 케이스가 해당 워크플로우의 모든 부분을 포함하도록 노력하세요. 이는 종종 여러 작업과 단언으로 이어지며, 이는 괜찮습니다.
이 예제들에 대한 실행 가능한 코드는 여기에서 찾을 수 있습니다.
act 경고가 발생합니다!act 유틸리티는 React Testing Library에 내장되어 있습니다. React Testing Library의 비동기 유틸리티를 사용하고 있다면 직접 act를 사용해야 하는 경우는 거의 없습니다.jest.useFakeTimers()를 사용할 때useImperativeHandle을 사용하고 상태 업데이트 함수를 호출하는 함수를 직접 호출할 때act 경고가 발생한다면, 가장 가능성 높은 이유는 테스트가 완료된 후에 무언가가 발생하고 있고, 여러분이 그것을 기다려야(wait) 하기 때문입니다.// 🛑 이렇게 하지 마세요...test('course shows loading screen', () => { getCourseInfo.mockResolvedValueOnce(buildCourse()) render(<Course courseId="123" />) const alert = screen.getByRole('alert') expect(alert).toHaveTextContent(/loading/i)})Course를 렌더링하고 로딩 메시지가 제대로 표시되는지 확인하려고 합니다. 문제는 <Course /> 컴포넌트를 render할 때 즉시 비동기 요청이 발생한다는 것입니다. 우리는 이를 올바르게 모킹하고 있습니다(그렇게 하지 않으면 테스트가 실제로 요청을 보낼 것이기 때문에 반드시 해야 합니다). 하지만 모킹된 요청이 해결(resolve)될 기회를 갖기도 전에 테스트가 동기적으로 완료됩니다. 마침내 요청이 해결되면 성공 핸들러가 호출되어 상태 업데이트 함수를 호출하고, 우리는 act 경고를 받게 됩니다.wait 유틸리티 사용하기// 1. 프로미스가 해결될 때까지 기다리기// ⚠️ 이 방법도 문제를 해결하는 괜찮은 방법이지만, 더 좋은 방법이 있으니 계속 읽어보세요.test('course shows loading screen', async () => { const promise = Promise.resolve(buildCourse()) getCourseInfo.mockImplementationOnce(() => promise) render(<Course courseId="123" />) const alert = screen.getByRole('alert') expect(alert).toHaveTextContent(/loading/i) await act(() => promise)})// 2. React Testing Library의 `wait` 유틸리티 사용하기test('course shows loading screen', async () => { getCourseInfo.mockResolvedValueOnce(buildCourse()) render(<Course courseId="123" />) const alert = screen.getByRole('alert') expect(alert).toHaveTextContent(/loading/i) await wait()})mockResolvedValueOnce를 사용하는 경우) 그러할 것입니다. 여기서는 act를 직접 사용할 필요는 없지만, 이 테스트는 기본적으로 기다리는 동안 발생한 모든 일을 무시하고 있기 때문에 별로 권장하지 않습니다.아직 댓글이 없습니다.
첫 번째 댓글을 작성해보세요!
![[번역] Claude Code에서 이용 제한에 빨리 걸리지 않게 하는 팁](https://res.cloudinary.com/zenn/image/upload/s--bpMrIbNq--/c_fit%2Cg_north_west%2Cl_text:notosansjp-medium.otf_55:Claude%2520Code%25E3%2581%25A7%25E3%2581%2599%25E3%2581%2590%25E5%2588%25B6%25E9%2599%2590%25E3%2581%25AB%25E3%2581%258B%25E3%2581%258B%25E3%2582%2589%25E3%2581%25AA%25E3%2581%2584%25E3%2582%2588%25E3%2581%2586%25E3%2581%25AB%25E3%2581%2599%25E3%2582%258B%25E3%2582%25B3%25E3%2583%2584%2Cw_1010%2Cx_90%2Cy_100/g_south_west%2Cl_text:notosansjp-medium.otf_34:%25E5%2590%2589%25E5%25B2%25A1%25E8%25A3%2595%25E8%25B2%25B4%2Cx_220%2Cy_108/bo_3px_solid_rgb:d6e3ed%2Cg_south_west%2Ch_90%2Cl_fetch:aHR0cHM6Ly9zdG9yYWdlLmdvb2dsZWFwaXMuY29tL3plbm4tdXNlci11cGxvYWQvYXZhdGFyLzUwM2RkNmMyMzkuanBlZw==%2Cr_20%2Cw_90%2Cx_92%2Cy_102/co_rgb:6e7b85%2Cg_south_west%2Cl_text:notosansjp-medium.otf_30:AUN%2520Tech%2520Blog%2Cx_220%2Cy_160/bo_4px_solid_white%2Cg_south_west%2Ch_50%2Cl_fetch:aHR0cHM6Ly9zdG9yYWdlLmdvb2dsZWFwaXMuY29tL3plbm4tdXNlci11cGxvYWQvYXZhdGFyLzFmNzI5YzVhODEuanBlZw==%2Cr_max%2Cw_50%2Cx_139%2Cy_84/v1627283836/default/og-base-w1200-v2.png?_a=BACAGSGT)
[번역] Claude Code에서 이용 제한에 빨리 걸리지 않게 하는 팁
Inkyu Oh • AI & ML-ops
![[번역] JSX에서 && 대신 삼항 연산자를 사용하세요](https://res.cloudinary.com/kentcdodds-com/image/upload/$th_1256,$tw_2400,$gw_$tw_div_24,$gh_$th_div_12/co_rgb:a9adc1,c_fit,g_north_west,w_$gw_mul_14,h_$gh,x_$gw_mul_1.5,y_$gh_mul_1.3,l_text:kentcdodds.com:Matter-Regular.woff2_50:Check%2520out%2520this%2520article/co_white,c_fit,g_north_west,w_$gw_mul_13.5,h_$gh_mul_7,x_$gw_mul_1.5,y_$gh_mul_2.3,l_text:kentcdodds.com:Matter-Regular.woff2_110:Use%2520ternaries%2520rather%2520than%2520%2526%2526%2520in%2520JSX/c_fit,g_north_west,r_max,w_$gw_mul_4,h_$gh_mul_3,x_$gw,y_$gh_mul_8,l_kent:profile-transparent/co_rgb:a9adc1,c_fit,g_north_west,w_$gw_mul_5.5,h_$gh_mul_4,x_$gw_mul_4.5,y_$gh_mul_9,l_text:kentcdodds.com:Matter-Regular.woff2_70:Kent%20C.%20Dodds/co_rgb:a9adc1,c_fit,g_north_west,w_$gw_mul_9,x_$gw_mul_4.5,y_$gh_mul_9.8,l_text:kentcdodds.com:Matter-Regular.woff2_40:kentcdodds.com%252Fblog%252Fuse-ternaries-rather-than-and-and-in-jsx/c_fill,ar_3:4,r_12,g_east,h_$gh_mul_10,x_$gw,l_unsplash:photo-1516824600626-47a22f894aff/c_fill,w_$tw,h_$th/kentcdodds.com/social-background.png)
[번역] JSX에서 && 대신 삼항 연산자를 사용하세요
Inkyu Oh • Front-End
![[번역] 타입 세이프한 합성 컴포넌트 만들기](https://tkdodo.eu/blog/og-images/building-type-safe-compound-components.png)
[번역] 타입 세이프한 합성 컴포넌트 만들기
Inkyu Oh • Front-End
[번역] sessionStorage가 다른 탭 간에 공유되지 않는 이유
Inkyu Oh • Front-End