이전 블로그에서는 Jest와 MSW를 이용하여 회원가입 테스트를 진행해보았다.
이제는 E2E 테스트의 대표적 선두주자인 Cypress로 회원가입 E2E 테스트를 진행해보기로 했다.
Cypress 설치
yarn add -D cypress
그 어느 패키지와 마찬가지로 위 명령어를 통해 설치를 시도했다.
근데 위 명령어로 설치를 하고 cypress를 구동하면 계속
No version of cypress is installed 라는 에러가 떴다.
나는 프로젝트에서 Next 14와 Typescript v5를 사용하고 있는데 호환이 안되는 것 같았다.
구글링 결과
./node_modules/.bin/cypress install
위 명령어를 통해 cypress의 바이너리 파일을 다운로드 해주면 문제를 해결할 수 있다고 했다.
그 후 package.json 파일의 scripts에
"cypress": "npx cypress open"
위 스크립트를 추가한 후 yarn cypress를 통해 cypress를 구동해보자.
그러면 위처럼 cypress를 구동시킬 브라우저를 선택하려는 창이 나오고, 나는 보편적인 크롬을 선택해주었다.
그럼 위처럼 spec을 생성할 수 있는 창이 나온다. 오른쪽에 Create new spec을 클릭해보자.
그럼 위처럼 spec 파일의 이름을 정할 수 있는 창이 나오게 된다.
원하는 이름의 파일을 생성해보자.
나는 회원가입 페이지를 테스트 할 것이므로 register.cy.ts로 이름을 정한 후 생성하였다.
그러면
위처럼 '/cypress/e2e' 경로에 내가 지정한 이름의 spec 파일이
describe('template spec', () => {
it('passes', () => {
cy.visit('https://example.cypress.io')
})
})
위의 디폴트 내용을 가지고 생성되어있는 것을 확인할 수 있을 것이다!
내 프로젝트는 모바일 화면만 지원하고 있으므로 테스트환경도 모바일 크기에 맞게 맞춰주어야 했다.
import { defineConfig } from "cypress";
export default defineConfig({
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
},
baseUrl: "http://localhost:3000",
},
viewportWidth: 400,
viewportHeight: 780,
});
생성된 cypress.config.ts 파일에서 viewportWidth, viewportHeight 속성을 통해 테스트 화면 크기를 지정해주었다.
E2E 테스트란?
End To End 테스트의 약자로 애플리케이션의 흐름을 처음부터 끝까지 테스트하는 것을 의미한다.
유닛 테스트나 통합 테스트는 모듈의 무결성을 증명할 수 있는 강력한 테스트이지만, 모듈의 무결성이 애플리케이션 동작의 무결성을 보장하는 것은 아니다.
그래서 E2E 테스트 과정에서는 실제 사용자의 시나리오를 테스트함으로써 애플리케이션 동작을 테스트하게 되고, 이 테스트를 통과함으로써 애플리케이션의 무결성을 증명할 수 있게 된다고 카카오 블로그에 친절히 작성되어 있다.
로컬 회원가입 페이지 접근 테스트
이제 본격적으로 로컬 회원가입 페이지의 E2E 테스트를 진행해봐야 한다.
우선 회원가입에 접근하는 걸로 가볍게 시작해보자~!!
describe("로컬 회원가입 화면", () => {
it("사용자는 이메일, 비밀번호, 닉네임을 사용해서 가입한다.", () => {
//given: 회원가입 페이지에 접근한다.
cy.visit("/register");
});
});
사용법이 전체적으로 jest와 매우 유사하다.
로컬 서버를 구동시킨 후 cy.visit()를 통해 회원가입 페이지에 접근해보도록 했다.
spec에 접근해보면
위처럼 회원가입 페이지 접근 테스트가 잘 통과하는 것을 확인할 수 있다.
input들 입력 테스트
그 후 이메일, 비밀번호, 닉네임 input에 값을 입력하는 과정을 테스트해보자.
describe("로컬 회원가입 화면", () => {
it("사용자는 이메일, 비밀번호, 닉네임을 사용해서 가입한다.", () => {
//given: 회원가입 페이지에 접근한다.
cy.visit("/register");
//when: 이메일, 비밀번호, 닉네임을 입력하고 가입하기 버튼을 클릭한다
cy.get("[data-cy=email-input]").as("emailInput");
cy.get("[data-cy=password-input]").as("passwordInput");
cy.get("[data-cy=nickname-input]").as("nicknameInput");
cy.get("@emailInput").type("brian990614@naver.com");
cy.get("@passwordInput").type("Gusals990^^");
cy.get("@nicknameInput").type("brian");
cy.get("@emailInput").invoke("val").should("eq", "brian990614@naver.com");
cy.get("@passwordInput").invoke("val").should("eq", "Gusals990^^");
cy.get("@nicknameInput").invoke("val").should("eq", "brian");
});
});
위처럼 cy.get()에서 data-cy 값을 통해 각 input에 접근한 것을 볼 수 있다.
<FormInput
dataCy="email-input"
label="아이디 (이메일)"
field="email"
errorMsg={getErrorMessage("email")}
onChange={handlers.changeEmail}
className="mb-[3.2rem]"
/>
내 경우 FormInput이라는 공통 컴포넌트로 input을 관리하고 있기 때문에 위처럼 dataCy prop을 넘겨준 뒤,
<div className={twMerge("flex flex-col", className)}>
<label htmlFor={field} className="text-text-gray-6 body2-semibold">
{label}
</label>
<input
data-cy={dataCy}
id={field}
type={type}
value={text}
className="w-full py-[6px] pl-[0.4rem] border-b-[1px] border-text-gray-6 mt-[9px] body2-medium"
onChange={handleInputChange}
/>
<FormErrorMsg testId={`${field}-error`} errorMsg={errorMsg} />
</div>
위처럼 input JSX 태그에 data-cy 값을 직접 넣어주었다.
그리고 as()를 사용하여 alias를 설정해 줄 수 있다.
설정해준 alias를 이용하여 type()을 통해 각 input에 테스트용 값을 입력해주었다.
추가로 테스트로 입력한 값이 의도한 값과 맞게 설정되었는지 재확인도 진행주었다.
invoke('val')을 통해 HTML요소의 value 속성 값을 가져올 수 있고, should()를 통해 내가 의도한 값과 맞는지 확인할 수 있다.
여러가지 chainers 중, 'eq'를 설정해주면 value값과 일치하는지 확인해볼 수 있다.
위와 같이 테스트가 잘 통과되는 것을 확인할 수 있다!
정말 실제로 유저가 사용하는 것 처럼 테스트가 수행되는 것을 볼 수 있다.
가입하기 클릭 시 로딩 UI가 뜨고, 성공 시 가입 성공 페이지로 이동 테스트
이제 마지막 로직이 남았다.
가입하기 클릭을 눌렀을 때, 로딩 UI가 바로 뜨고 가입 요청이 성공되면 가입 성공 페이지로 이동해야한다.
cy.intercept(
{
method: "POST",
url: "/api/auth/register",
},
{
statusCode: 200,
}
).as("register");
cy.get("[data-cy=register-button]").should("exist").click();
cy.get("[data-cy=loading-ui]").should("exist");
cy.wait("@register").then((interception) => {
if (interception && interception.response) {
expect(interception.response.statusCode).to.eq(200);
}
});
// then: 가입 성공 페이지로 넘어간다
cy.url().should("include", "/register/success");
위 테스트코드에서 추가된 부분이다.
로직에서 가입하기 버튼을 누르면 '/api/auth/register' 경로의 route handler를 호출한 후, 해당 경로의 route.ts 파일 내에서 백엔드와 통신이 일어난다.
해당 통신에서 status 200이 return되면 가입 성공 페이지로 이동하게 된다.
이 때 실제 서버에 통신이 가지 않도록 MSW를 사용해서 했던 것 처럼 http request를 mocking 해야한다.
여기서 cypress가 왜이렇게 인기가 많은지 알 수 있었다.
Jest와 MSW 환경에서 http request를 mocking하기 위해 했던 수많은 세팅 과정들이 cypress에서는 전혀 요구되지 않았다.
그저 intercept() 하나면 완료되었다. 네이밍도 직관적이여서 좋았다.
POST 메소드 요청을 intercept()를 통해 mocking 해준 뒤, 뒤 로직에서 사용할 alias로 처리 해주었다.
intercept() 사용법은 매우 다양해서 https://docs.cypress.io/api/commands/intercept 공식문서를 보면 도움이 될 것 같다.
http requst도 mocking 해 주었으니 이제 다 되었다.
- 버튼을 가져와서 클릭
- 로딩 UI가 뜨는지 확인
- 응답이 status 200으로 오는 것 확인
- 이동한 url에 '/register/success' 가 포함된는지 확인
이를 순서대로 테스트해주기 위해 코드를 작성했고, 최종 테스트코드는 다음과 같다.
describe("로컬 회원가입 화면", () => {
it("사용자는 이메일, 비밀번호, 닉네임을 사용해서 가입한다.", () => {
//given: 회원가입 페이지에 접근한다.
cy.visit("/register");
//when: 이메일, 비밀번호, 닉네임을 입력하고 가입하기 버튼을 클릭한다
cy.get("[data-cy=email-input]").as("emailInput");
cy.get("[data-cy=password-input]").as("passwordInput");
cy.get("[data-cy=nickname-input]").as("nicknameInput");
cy.get("@emailInput").type("brian990614@naver.com");
cy.get("@passwordInput").type("Gusals990^^");
cy.get("@nicknameInput").type("brian");
cy.get("@emailInput").invoke("val").should("eq", "brian990614@naver.com");
cy.get("@passwordInput").invoke("val").should("eq", "Gusals990^^");
cy.get("@nicknameInput").invoke("val").should("eq", "brian");
cy.intercept(
{
method: "POST",
url: "/api/auth/register",
},
{
statusCode: 200,
}
).as("register");
cy.get("[data-cy=register-button]").should("exist").click();
cy.get("[data-cy=loading-ui]").should("exist");
cy.wait("@register").then((interception) => {
if (interception && interception.response) {
expect(interception.response.statusCode).to.eq(200);
}
});
// then: 가입 성공 페이지로 넘어간다
cy.url().should("include", "/register/success");
});
});
이제 spec을 한번 봐보자.
위처럼 E2E 테스트가 잘 돌아가는 것을 확인할 수 있다!!
이제 적어도 유저가 input들을 올바르게 입력 후, 통신이 성공한다면 무사히 가입 성공 페이지로 이동하는 것을 보장할 수 있게 되었다.
추가로 status 400 처럼 실패했을 경우를 가정하여 E2E 테스트를 진행한다면 더 탄탄한 서비스가 될 것이다.
Jest & Cypress & MSW
그럼 이제 가장 큰 의문이 남았다.이렇게 훌륭한 도구들 사이에서 뭘 사용해야하지...?
Jest & MSW 사용
Jest는 Facebook 에서 만든 훌륭하고 안정적인 테스트코드 도구이다.그렇기에 실제로 엄청나게 많은 기업들이 Jest를 활용하고 있다(네이버, 토스, 카카오 등 우리나라 개발자들이 항상 꿈꾸는 기업들은 실제로 Jest 사용자를 우대하기도 한다).또한 유닛 테스트, 통합 테스트에는 그 어떠한 도구보다 적합하다는 평가를 받는다.
그렇지만 역시 단점도 있다.도입하며 느낀 가장 큰 단점은 테스트를 시작할 때 드는 세팅 비용이 너무 크다는 점이였다.버그를 줄이기 위해 테스트 도구를 도입하는 것인데, 세팅을 하며 버그가 오히려 더 많아지는 느낌이 들 정도였다..ㅋㅋ. http mocking을 위해 MSW를 세팅하는 과정도 상당히 복잡했다.
Cypress 사용
Cypress는 리액트 프로젝트에서 범접할 수 없는 E2E 테스트 도구이다.
Jest에 비해서는 아니지만 그래도 많은 기업들이 선호하고 좋아하는 도구이다.
사실 사용자 flow를 그대로 시뮬레이션 할 수 있다는 점이 가장 테스트에서 이상적인 모습이 아닐까 생각했다.
또한 세팅, http request mocking 등 Jest에서는 복잡했던 과정들이 매우 매우 간편했다.
사용자가 실제 사용하는 것처럼 시뮬레이션 하는 과정을 자동으로 UI에서 빠른 속도로 볼 수 있다는 점도 매우 강력한 장점이였다.
하지만 아직은 얕은 지식으로 이유는 모르겠지만 비교할 수 없을만큼 많은 수의 기업들이 Jest를 사용하고 있다. 그래서 Jest를 포기하기에는 찝찝해...
또한 유닛 테스트, 통합 테스트에 Jest보다는 적합하지 않은 도구로 판단되고 있다(하긴 end to end 테스트에서 세부 테스트까지 하면 그게 더 이상할 것 같다).
결론
우선 내 결론은 일단 둘 다 사용하고 있자라는 거였다.
강의에서는 물론 둘 다 사용하는 것을 반대하고 있기는 하다.
강의자분의 경험에 비추었을 때는 둘 다 사용했을 때 러닝 커브가 너무 높아질 수 있다고 하셨다.
또한, 유닛 테스트 / 통합 테스트 / E2E 테스트의 범주를 나누는 것이 너무 어려워질 수도 있다고 하셨다.
하지만 당장 내 프로젝트에서는 위 문제들이 발생할 확률은 당장 없을 것이라고 판단되었기 때문에 둘다 일단 가져가는 것이 내 미래에도 좋을 것 같다는 생각을 했다.
Jest와 Cypress를 동시에 사용하려면 추가적인 설정이 필요하다.
루트 tsconfig.json 파일에
"exclude": ["node_modules", "./cypress.config.ts", "cypress"]
위 내용을 추가해줘야 한다. 그래야 cypress 세팅 내용이 jest에 얽히지 않는다.
그 후 cypress 폴더 내에 tsconfig.json 파일을 하나 더 만들어 주고
{
"extends": "../tsconfig.json",
"include": ["cypress", "../cypress.config.ts", "./**/*.ts"],
"exclude": ["node_modules"]
}
위처럼 설정해주었다.
그러면 루트 tsconfig.json 파일을 확장하되, cypress가 적용되는 범주이므로 루트에서는 exclude 했던 내용들을 다시 include 해주었다.
위처럼 세팅을 해주면 모든 에러가 사라지게 된다!
아무튼 결론!! 당분간은 유닛 테스트 / 통합 테스트는 Jest & MSW로, E2E 테스트는 Cypress로 진행해 나가면서 더 많은 것들을 배워나가야겠다고 생각했다~~
참고
https://fe-developers.kakaoent.com/2023/230209-e2e/
E2E 테스트 도입 경험기 | 카카오엔터테인먼트 FE 기술블로그
방경민(kai) 사용자들에게 보이는 부분을 개발한다는 데서 프론트엔드 개발자의 매력을 듬뿍 느끼고 있습니다.
fe-developers.kakaoent.com
'FrontEnd > Test Code' 카테고리의 다른 글
현업에서의 테스트코드 (0) | 2024.07.17 |
---|---|
프론트엔드 테스트에서 http통신 mocking 하기(Feat: MSW) (2) | 2024.06.27 |
Next에서 테스트코드 적용해보기 (Feat: Jest) (0) | 2024.06.24 |
테스트 코드 문법 익히기 (Feat: Jest) (2) | 2024.06.21 |
테스트 코드 이론 (0) | 2024.06.19 |