테스트는 우리가 작성한 코드가 예상대로 작동하는지 확인하는 과정을 의미한다.
가장 기본적이고 확실한 테스트 방법은 기능을 직접 사용하여 기능이 의도한대로 제대로 작동하는지 확인하는 것이다.
그러나 코드가 수정될 때마다 직접 테스트를 수행하는 것은 시간이 많이 걸리고, 실수할 가능성이 있다.
그래서 테스트 코드를 작성하여 테스트 과정을 자동화한다.
프론트엔드 테스트는 일반적인 테스트와 다르다.
비즈니스 로직을 담은 함수의 입출력(I/O)을 검증하거나 세부 구조 및 동작을 검증하는 것과 달리, 프론트엔드 테스트는 사용자 경험에 초점을 맞춘다.
즉 사용자가 발생시키는 이벤트(click, scroll, focus 등)에 대한 화면의 반응을 확인하는 과정이다. 이를 통해 적절한 화면이 출력되었는지 테스트한다.
이벤트 트리거는 react testing library와 같은 도구를 사용해 쉽게 수행할 수 있지만, 출력된 화면을 확인하는 것은 간단하지 않다.
사람이 직접 확인할 때는 화면이 기대와 동일한지 눈으로 확인하지만, 컴퓨터는 픽셀 단위로 비교해야 하며 이는 많은 시간을 소요하고 거짓 음성을 발생시킬 수 있다.
거짓 음성(false nagetive)이란 실제 결과는 성공했지만 테스트는 실패한 경우이다.
이는 사람의 눈에는 원하는 화면이 출력되었지만 픽셀에서 차이가 발생하기 때문에 발생한다.
따라서 픽셀 비교 대신 기능의 중요한 내용을 비교 단위로 삼는 방식이 존재한다.
예를 들어, 버튼을 누르면 특정 텍스트가 추가되는지 확인하는 것이다.
이는 픽셀 비교 방식보다는 정확성이 떨어질 수 있지만, 시간 소요와 거짓음성 문제를 해결할 수 있다.
픽셀 비교는 시각적 테스트, 기능 비교는 기능적 테스트라고 한다.
시각적 테스트에는 스냅샷 테스트와 시각 회귀 테스트가 있고,
기능적 테스트에는 단위 테스트, 통합 테스트, E2E 테스트가 있다.
테스트 코드를 작성하기 전에 지원하는 기능을 명확히 정의함으로써 사용자가 사용하는 기능이 제대로 작동할 것이라는 신뢰성이 생긴다.
TDD를 제안한 켄트 벡은 테스트 코드 작성을 불안함을 지루함으로 바꾸는 과정이라 했다.
애플리케이션이 지원하는 기능을 명확히 정의하여 테스트 코드를 정의하기 때문에 팀원이 어떤 기능을 개발하고 테스트하는지 한 눈에 확인할 수 있다.
즉 테스트 코드 파일 자체가 잘 작성된 개발 문서로 활용될 수 있다. 이는 협업에 있어 소통의 안정성을 더해준다.
단위테스트란 애플리케이션 안에 있는 개별적인 코드 단위를 테스트하는 방식이다.
보통 함수, 리액트 컴포넌트, 커스텀 훅을 테스트한다.
다른 코드의 유닛과 상호작용하는 것을 테스트하지 않는다.
입력값만을 이용해 출력값을 결정짓는 순수함수이면 좋다.
- 순수함수의 경우 오로지 입력값만으로 출력값이 정해지기 때문에 외부 상태값을 가짜로 만들어내지 않아도 되어서 테스트하기 쉽고, 가짜 값을 만들지 않다보니 가짜 값이 유효한 값인지 신경쓰지 않아도 되기 때문이다.
- 외부에 의존성이 있다면 의존성을 mocking(가짜로 대체)한다. 외부 의존성의 실패가 아닌 특정 유닛의 실패임을 단정하기 위함
- 격리된 unit test에서 실패를 쉽고 정확하게 파악할 수 있다.
근데 이는 사용자가 소프트웨어와 상호작용하는 방식과는 거리가 멀다.
또한 리팩토링하는 과정에서 실패할 수 있다.
=> 동작은 그대로이나 구현의 변경이 테스트 실패의 원인이 될 수 있다.
도메인과 관련이 높고, 의존성이 낮으면서 로직이 복잡한 함수가 최고의 대상이다.
지나치게 복잡한 코드는 리팩토링의 신호이다.
컨트롤러와 같이 외부의존성만을 다루는 코드는 테스트의 우선순위가 낮다.
// 컴포넌트 테스트 예시
const TextField = ({ title, onChange }) => {
return (
<div>
<label htmlFor={title}>{title}</label>
<input id={title} onChange={onChange}></input>
</div>
)
}
// 테스트
test("label을 클릭하면 input이 foucus 됩니다", async () => {
const title = "일련번호"
const onChange = jest.fn()
render(<TextField title="일련번호" onChange={onChange} />)
userEvent.click(screen.getByText("일련번호"))
expect(screen.getByLabelText(title)).toHaveFocus()
})
test("input에 값을 넣은 만큼 onChange 핸들러가 호출 됩니다", async () => {
const title = "일련번호"
const onChange = jest.fn()
render(<TextField title={title} onChange={onChange} />)
userEvent.click(screen.getByText(title))
userEvent.type(screen.getByLabelText(title), "hello")
expect(onChange).toHaveBeenCalledTimes(5)
})
// 커스텀 훅
export const useCarousel = (initialIndex: number, carouselLength: number) => {
const [step, setStep] = useState(
initialIndex < carouselLength ? initialIndex : 0
)
const prevStep = () => {
if (step === 0) return setStep(carouselLength - 1)
setStep(step - 1)
}
const nextStep = () => {
if (step === carouselLength - 1) return setStep(0)
setStep(step + 1)
}
return { step, prevStep, nextStep }
}
// 테스트
it("초기값이 캐러샐 길이보다 긴 경우 첫번째 step을 0으로 설정한다", () => {
const { result } = renderHook(() => useCarousel(5, 3))
expect(result.current.step).toBe(0)
})
})
it("검색어 입력후 검색 버튼을 클릭하면 검색 결과를 보여준다.", async () => {
render(<App />)
const inputBox = screen.getByRole("textbox")
const searchButton = screen.getByRole("button")
act(() => {
userEvent.type(inputBox, "테스트")
userEvent.click(searchButton)
})
await waitFor(() => {
expect(screen.getAllByText("테스트", { exact: false }).length).toBe(20)
})
})
it("검색어 입력후 검색 버튼을 클릭하였을때 결과가 없을 경우 결과가 없음을 보여준다", async () => {
render(<App />)
const inputBox = screen.getByRole("textbox")
const searchButton = screen.getByRole("button")
act(() => {
userEvent.type(inputBox, "검색 결과 없는 검색어")
userEvent.click(searchButton)
})
await waitFor(() => {
expect(screen.getByText("검색 결과가 없습니다")).toBeInTheDocument()
})
})
it("사용자가 로그인 페이지에 진입해 아이디 비밀번호입력후 엔터키를 눌러 로그인에 성공하는경우 홈으로 이동해 유저 프로필을 확인한다", function () {
// destructuring assignment of the this.currentUser object
const username = "test"
const password = "1234"
// 로그인 페이지 진입
cy.visit("/login")
// 아이디 입력 input에 username 입력
cy.get("input[name=username]").type(username)
// 패스워드 입력 input에 password 입력 및 enter 입력
cy.get("input[name=password]").type(`${password}{enter}`)
// home으로 리다이렉션
cy.url().should("include", "/home")
// home에 username이 있는지 확인
cy.get("h1").should("contain", username)
})
TDD란 Test Driven Development의 약자로 켄트 벡이 1999년 익스트림 프로그래밍의 일부로 제안하며 널리 알려졌다.
동작하는 코드 작성 이전에 테스트를 먼저 작성하고 그 테스트를 통과하는 코드를 작성함으로써 테스트된 동작하는 코드를 얻는 개발 방법이다.
https://testing-library.com/docs/react-testing-library/intro/
브라우저 없이 테스트하기 위해 시뮬레이션된 DOM을 생성하고, 이를 활용하여 DOM과 상호작용할 수 있는 유틸리티를 제공한다.
렌더링 이후, react testing library의 전역 객체 screen을 통해 시뮬레이션된 DOM에 액세스할 수 있다.
사용자가 실제로 소프트웨어를 사용하는 방식과 동일하게 테스트할 수 있도록 돕는다.
테스트에서 사용자가 버튼 클릭과 같은 작업을 하고 버튼을 클릭한 후 DOM이 어떤 모습인지 확인할 수 있다.
구현 세부 정보보다는 구성 요소의 동작을 테스트하는 데 중점을 두며, HTML 태그를 최대한 활용하여 테스트한다.
RTL이 공식문서에서 강조하는 원칙은 다음과 같다.
"테스트 코드를 작성하고 테스트를 수행함에 있어서 테스트가 서비스의 사용 방식과 유사할수록 더 많은 신뢰를 얻을 수 있다"
이는 즉 우리가 추구하는 테스트 목표와 동일하다.
getByRole
: 접근성 트리에 노출된 모든 요소를 조회하며, 가장 접근성과 사용자 경험을 고려하는 메서드이다.getByLabelText
: form 필드 내부 요소들을 각자의 label로 찾는다.getByPlaceholderText
: placeholder를 대체자로 사용한다.getByText
: 텍스트로 요소를 찾는다.getByDisplayValue
: form 내부에서 이미 값이 입력된 요소를 찾는다.getByAltText
: 요소 내에 alt 속성을 조회한다.getByTitle
: Title 속성을 조회한다.getByTestId
: 위의 방법이 모두 불가할 때 최후의 수단으로 사용한다.https://vitest.dev/
테스트를 찾고, 실행하며, 통과 여부를 결정하는 도구이다.
Jest와 문법이 동일하고 빌드 도구인 vite와 호환성이 좋다.
RTL은 시뮬레이션된 DOM을 리턴하며, Vitest와 같은 테스트 러너를 사용하여 테스트를 실행한다.
MSW 공식 문서
MSW는 브라우저 및 Node.js 환경에서 사용 가능한 API 모킹 라이브러리이다.
브라우저에서는 브라우저에서 제공하는 Service Worker를 이용해서 mocking 한다.
(Node.js 환경에서는 http, XMLHttpRequest와 같은 네트워크 프로토콜을 mocking 한다.)
브라우저에서의 원리
1. 브라우저가 요청을 한다.
2. Service Worker가 이를 인지한다.
3. Service Worker는 요청을 실제 서버로 보내지 않고 요청을 복사하여 클라이언트 사이드에 있는 MSW 라이브러리로 보낸다.
4. MSW는 해당 요청에 대한 handler를 찾아서 등록된 모의 응답값을 Service Worker를 통해 브라우저에게 전달한다.
5. 이를 통해 실제 서버 존재 여부와 상관없이 실제 요청으로 이어지지 않고 예상할 수 있는 요청에 대해 Mocking이 가능해진다.
https://github.com/goldbergyoni/javascript-testing-best-practices/blob/master/readme.kr.md
https://techblog.woowahan.com/17721/
https://techblog.woowahan.com/17404/
https://www.youtube.com/watch?v=R7spoJFfQ7U
https://www.youtube.com/watch?v=mIO4Rbe_M74
https://www.youtube.com/watch?v=rkTt1uQ1YHI
https://tech.kakaopay.com/post/implementing-tdd-in-practical-applications/
https://tech.kakao.com/posts/458
https://tech.madup.com/mock-service-worker/
https://tech.madup.com/front-test-tips/
https://developer-bandi.github.io/post/frontend-testing/
// kent c. dodds 글 번역
https://soojae.tistory.com/74
https://soojae.tistory.com/82?category=1010060
https://soojae.tistory.com/83?category=1010060
https://soojae.tistory.com/84?category=1010060
https://jaehyeon48.github.io/testing/avoid-nesting-when-youre-testing/
https://jymini.tistory.com/73
https://yozm.wishket.com/magazine/detail/2435/
https://yozm.wishket.com/magazine/detail/2483/
https://im-developer.tistory.com/226
https://velog.io/@sehyunny/a-compilation-of-outstanding-testing-articles
https://musma.github.io/2023/07/24/front-end-test-code.html
https://ykss.netlify.app/translation/unit-testing-with-jest-react-and-typescript/
https://github.com/ssi02014/react-test-reference-documentation?tab=readme-ov-file
AHA 법칙 Avoid Hasting Abstraction
https://jaehyeon48.github.io/testing/avoid-nesting-when-youre-testing/
https://kentcdodds.com/blog/aha-testing