# 개발할때 편하다
yarn add -D @types/cypress
// react-devtools 같은 확장도구가 필요할 경우
// /plugins/index.js
const path = require('path')
module.exports = (on, _config) => {
on('before:browser:launch', (browser, launchOptions) => {
if (browser.family === 'chromium') {
const extensionFolder = path.resolve(__dirname, '..', '..', '4.7.0_1')
launchOptions.args.push(`--load-extension=${extensionFolder}`)
return launchOptions
}
})
}
// /support/commands.js
Cypress.on('window:before:load', (win) => {
win.__REACT_DEVTOOLS_GLOBAL_HOOK__ = window.top.__REACT_DEVTOOLS_GLOBAL_HOOK__
})
// 파일업로드 기능 테스트
Cypress.Commands.add(
'uploadFile',
{ prevSubject: true },
(subject, fileName) => {
cy.fixture(fileName).then((content) => {
const el = subject[0]
const testFile = new File([content], fileName)
const dataTransfer = new DataTransfer()
dataTransfer.items.add(testFile)
el.files = dataTransfer.files
cy.wrap(subject).trigger('change', { force: true })
})
}
)
// 에러때문에 테스트가 끊길 경우
Cypress.on('uncaught:exception', (err, runnable) => {
console.log(err)
return false
})
테스트 코드에서 mock으로 처리하는 경우1
- Mocking React custom hook with Jest - Stack Overflow
- mrbenhowl/mocking-firebase-initializeApp-and-firebase-auth-using-jest
- Avoid Nesting when you’re Testing
Footnotes
-
그래서 내가 내린 결론은 저렇게 까지는 테스트할 필요가 없고 오히려 애매하게 결합된 컴포넌트들을 분리해서 관리하는게 맞을 것 같다는 생각을 해봤다. ↩
**title**: 반품 및 교환은 재고로 이동합니다.
**as a** 가게 주인으로서,
**I want** 반품 또는 교환 시 상품을 재고에 다시 추가하고 싶습니다.
**so that** 인벤토리를 추적할 수 있습니다.
**Scenario 1:** 환불을 위해 반품된 항목은 인벤토리에 추가되어야 합니다.
**Given** 고객이 이전에 나에게서 검은색 스웨터를 구매했다는 점을 감안할 때
**and** 인벤토리에 검은색 스웨터 3개가 있습니다.
**when** 환불을 위해 검은색 스웨터를 반환할 때
**then** 그러면 인벤토리에 4개의 검은색 스웨터가 있어야 합니다.
Feature > […Scenario] > […Step]
Given
: Given단계는 시스템의 초기 컨텍스트( 시나리오 장면) 를 설명하는 데 사용됩니다 . 그것은 일반적으로 과거에 일어난 일입니다.When
: When단계는 이벤트 또는 작업 을 설명하는 데 사용됩니다 . 이것은 시스템과 상호 작용하는 사람이거나 다른 시스템에 의해 트리거되는 이벤트일 수 있습니다.Then
: Then단계는 예상되는 결과 또는 결과 를 설명하는 데 사용됩니다 .And
,But
Scenario: 항목 검색 후 결제 페이지로 이동
Given 사용자가 Greencart 방문 페이지에 있다
When 사용자가 야채<이름>를 검색했을 때
And 장바구니에 항목이 추가된다
And 사용자는 구매를 위해 Checkout 페이지로 이동한다
Then 선택한 <이름> 항목이 체크아웃 페이지에 표시된다
connect가 실행된 컴포넌트를 (enzyme) mount
로 테스트한 경우 실패 케이스가 발생해서 찾아본 내용들 인 것 같은데 정확하게 기억이 안남. shallow
로 변경했더니 이번에는 ref를 못쓰는 문제도 있었다고 하고…
여담이지만 컴포넌트를 명확하게 정의하고 분리해서 테스팅 스트레스를 줄이는 게 중요합니다.
BDD는 무엇을 의미합니까?
BDD는 시스템의 원하는 동작을 협력적으로 지정하는 접근 방식입니다. 행동의 일부가 합의될 때마다 우리는 그 행동을 구현할 코드의 개발을 “추진”하기 위해 해당 사양을 사용합니다.
BDD의 세 가지 관행은 무엇이며 스토리에 어떤 순서로 적용합니까?
우리는 스토리에 필요한 행동의 범위를 공동으로 발견 하는 것으로 시작합니다. 일단 우리가 행동에 동의하면 우리는 비즈니스가 읽을 수 있는 언어로 사양을 공식화 합니다. 마지막으로 공식화된 사양을 자동화 하여 시스템이 실제로 예상대로 작동하는지 확인합니다.
Cucumber와 BDD는 어떤 관련이 있습니까?
Cucumber는 문서를 이해하고 자동화된 테스트로 변환하는 도구입니다.
BDD는 세 가지 방식으로 구성된 협업적 접근 방식입니다. BDD 실무자는 Cucumber를 사용하여 문서를 자동화할 수 있습니다.
“살아있는 문서”의 특별한 점은 무엇입니까?
문서가 애플리케이션의 동작과 동기화되지 않을 때 자동으로 알려 주기 때문에 “살아있는 문서”라고 부릅니다. 그것이 특별한 점입니다.
완료에 대한 정의의 일부로 이를 검토할 수 있지만 자동으로 유효성이 검사되지 않더라도 작성한 모든 사양 문서에 대해서도 마찬가지입니다.
그것은 자동화된 테스트에 의해 생성 되지 않습니다 - 여전히 작성해야 합니다! 자동화된 테스트는 귀하가 작성한 내용이 사실인지 아닌지를 알려줍니다.
이를 위한 변경 제어 프로세스가 있을 수 있습니다. 설명하는 코드와 함께 소스 제어에 유지하는 것이 좋습니다. 그러나 다시 말해서 특별한 것은 아닙니다. Word 문서에 대해 놀랍도록 투명한 변경 제어 프로세스를 가질 수 있지만 여전히 완전히 구식이고 잘못된 것일 수 있습니다.
최근에 Enzyme을 제거하고 React Testing Library로 교체하는 작업을 진행했습니다. JSX 영역은 별 문제 없이 진행되었으나, state나 props를 다루는 구현 부분에서 약간의 애매함이 있었습니다. 개인적으로는 기존 접근 방식이 나쁘지 않았다고 생각했기 때문에, 새로운 접근 방식으로 전환하는 데 주저하게 되었습니다.
결론적으로, state와 props의 JSON 결과물을 렌더링하고 getByTestId를 사용하여 이를 참조하는 방식으로 테스트를 진행하기로 했습니다. 다음은 그 예시입니다:
render() {
return <>
{process.env.NODE_ENV === 'test' && (
<pre data-testid="ThumbnailDebug">
{JSON.stringify({
props: this.props,
state: this.state,
})}
</pre>
)}
</>
}
이 방법을 통해 Enzyme을 제거한다는 목표를 달성했기 때문에, 어느 정도는 해결된 것처럼 보이며, 추후 더 나은 테스트 코드를 작성하기 위한 고민을 할 수 있을 것 같습니다.
React 애플리케이션에서 라우팅은 중요한 부분. 특히, 사용자가 링크를 클릭할 때 적절한 페이지로 이동하는지 테스트하는 것은 중요. 다음은 React Router의 <Link />
컴포넌트를 테스트하는 코드.
describe('Link', () => {
it('이동', () => {
const Home = () => <Link to="/about">About</Link>
const About = () => <h1>About</h1>
const { getByRole } = render(
<MemoryRouter initialEntries={['/']}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</MemoryRouter>
)
fireEvent.click(getByRole('link'))
expect(screen.getByRole('heading')).toHaveTextContent('About')
})
})
가끔 <Link />
도 테스트해야 할 때가 있다. 위 코드는 사용자가 “About” 링크를 클릭하면 “About” 페이지로 이동하는지 테스트. fireEvent.click을 사용해 링크 클릭 이벤트를 발생시키고, screen.getByRole을 통해 헤딩 요소의 텍스트가 ‘About’인지 확인. 이 테스트를 통해 라우팅이 제대로 동작하는지 확인할 수 있음.
vitest와 jest에서 가짜 타이머를 사용하는 방법. 내부적으로는 @sinonjs/fake-timers를 사용. vitest와 jest 모두 시스템 시간을 조작할 수 있도록 하여 테스트가 일관되게 실행되도록 하고 다양한 시간과 날짜를 시뮬레이션하여 다양한 조건에서 코드가 예상대로 작동하는지 확인할 수 있음.
describe('isSameDate', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime('2024-07-20')
})
afterEach(() => {
vi.useRealTimers()
})
it('현재 날짜와 동일한 날짜를 전달하면 true를 반환한다', () => {
expect(isSameDate('2024-07-20')).toBe(true)
})
it('현재 날짜와 다른 날짜를 전달하면 false를 반환한다', () => {
expect(isSameDate('2024-07-19')).toBe(false)
expect(isSameDate('2024-07-21')).toBe(false)
})
})
vitest에서 사용할 수 있는 사용자 지정 assertion을 추가하기.
import { expect } from 'vitest'
import type { ZodTypeAny } from 'zod'
expect.extend({
/**
* @param received 테스트할 Response 객체
* @param schema 검증할 Zod 스키마
*/
async toMatchSchema(received: Response, schema: ZodTypeAny) {
const response = await received.json()
const result = await schema.safeParseAsync(response)
return {
message: () => '',
pass: result.success,
} satisfies ExpectationResult
},
})
vitest.d.ts
에 기본 인터페이스를 확장하기.
import type { ZodTypeAny } from 'zod'
interface CustomMatchers<R = unknown> {
toMatchSchema(schema: ZodTypeAny): Promise<R>
}
todoResponse
의 응답 데이터가 todoSchema
에 정의된 Zod 스키마와 일치하는지 확인한다.
test('todo', async () => {
expect(todoResponse.ok).toBeTruthy()
expect(todoResponse).toMatchSchema(todoSchema)
})