일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- react-ga
- 헤드퍼스트전략패턴
- 리팩토링2판4장
- 가상면접2장
- 시스템설계
- awss3
- formik submitting not working
- Git commit 합치기
- cypressBDD
- 테스트코드책
- file not found Error
- react
- 리팩터링2판테스트
- git commit 협업
- 시스템설계면접팁
- 리액트구글애널리틱스
- 디자인패턴
- FirebaseAnalytics
- 가상면접3장
- gitsquash
- cypress React
- 시스템설계면접예시
- formik react-query submitting not working
- 가상면접으로대규모시스템
- 시스템설계면접
- git commit merge
- s3이미지다운로드됨
- git squash
- 전략패턴
- 시스템설계방법
- Today
- Total
mingg IT
[Cypress] NX + React+ MUI + TS+ Formik Cypress 도입에 대한 고찰 본문
- 테스트 코드의 필요성
- Cypress를 선택한 이유
- Cypress 실행 명령어
- 테스트 범위
- Cypress에 대한 한계점과 고민점
- 테스트 코드 작성 과정
- 느낀 점
테스트 코드의 필요성
안정성 확보(어제 되던 api가 오늘은 안됨), 유지보수 시간 단축, 개발 시간 단축, 프로젝트 협업자들 간의 공유
초기 컴포넌트를 개발하는데 시간을 절약하고 싶다면 Storybook을 사용하고,
리팩토링, 유지보수 등에 시간을 절약하고 싶으면 E2E 테스트를 적용하는 게 좋다.
E2E 테스트 왜 Cypress를 선택했나?
Cypress | TestCafe | |
크로스 브라우징 호환 | X | O |
비디오 지원 여부 | O | O |
에러 핸들링 | X | O |
API 다양화 | O | X |
NX 지원 여부 | O | X |
여러 장단점이 있지만, 우리가 진행하고 있는 프론트엔드 프로젝트가 NX 기반으로 개발 중이었다.
NX에서 프로젝트를 추가하게 되면 자동으로 e2e 프로젝트도 추가되기 때문에
굳이 만들어준 cypress 기반 프로젝트를 지우고, TestCafe를 사용할 이유가 없다.
명령어
yarn nx e2e vaunce-app-e2e
테스트에 대한 비디오랑 스냅샷을 남겨준다.
스냅샷 같은 경우에는 에러가 발생한 부분만 보여주는데 어떤 테스트 케이스, 어디서 터졌는지 캡처해서 보여준다.
GUI로 보고 싶을 경우 & 테스트코드를 작성하는 중 일 경우
yarn nx e2e vaunce-app-e2e --watch
어디까지 테스트할래?
현재 진행하는 프로젝트의 프론트엔드를 개발할 때 체크해야 할 항목들은 다음과 같다.
1. HTML, css와 같은 초기 UI 컴포넌트
2. 사용자 입력에 따른 유효성 검사 (Helptext, 버튼 활성화 등 Formik 검사)
3. API 정상 작동 유무와, API 응답 결과에 따른 화면
4. 사용 시나리오에 따른 테스트
사실 모두 하면 당연히 좋겠지만, 시간이 한정적으로 있는 상황에서 우선순위를 정했다.
3 > 2 > 4 > 1
언제든지 변할 가능성이 높은 초기 UI 컴포넌트 테스트를 제일 하로 두었다.
테스트 코드를 작성하면서 아쉬웠던 점과 고민했던 점
아쉬웠던 점
- 회원가입을 위한 본인인증, 결제모듈에서 사용하는 NicePay 등 다른 웹 애플리케이션 환경이 이용되는 경우에는 적용할 수 없다.
- MUI 자체가 HTML 여러 개 뭉친 집합이라, Selector(cy.get(['data-cy']))를 이용하여 테스트 코드를 작성하는데 시간이 좀 걸림.
- 테스트 케이스를 도는데 시간이 오래 걸림
고민했던 점
- 어떤 시나리오에 대한 테스트 코드를 작성 할 것인가?
- 테스트 케이스를 직관적으로 확인하는 방법
- 테스트 파일(cy.ts)을 어떤식으로 나눌 것인가
- 모듈 별로 나눌 것인가
- 시나리오 별로 나눌것인가
- E2E(API 정상 작동+ 사용 시나리오), UI(UI + Formik) 두가지 버전으로 나눌 것인가
고민에 대한 해결 방안
어떤 시나리오에 대한 테스트 코드를 작성할 것인가?
- 핵심 기능, 이슈가 발생했던 로직, 내가 가장 자신 없는 로직 먼저 작성한다.
- QA를 진행하면서 이슈로 나왔던 항목을 적는 것도 좋은 방법임
시설 이용 등록 부분에서 이슈가 가장 많이 발생했었고, 경우의 수가 많은 부분이라 가장 먼저 테스트 코드를 적용하였다.
테스트 케이스를 직관적으로 확인하는 방법
- 한 파일 내 테스트 케이스가 여러 개인데 테스트 케이스를 하나씩 돌릴 수가 없다.
- describe를 중첩해서 사용하면 분기처리를 통해 좀 더 직관적으로 확인할 수 있다.
테스트 파일(cy.ts)을 어떤 식으로 나눌 것인가?
- 모듈 별(기능 별)로 나눌 것인가 (X)
- 사용자는 특정 모듈만 사용하는 것이 아니다. 모듈간 연관성을 테스트 할 수 없다.
- E2E(API 정상 작동+ 사용 시나리오), UI(UI + Formik) 두가지 버전으로 나눌 것인가 (X)
- 처음엔 한 파일에 너무 많은 케이스가 생겨버려서 E2E, UI 로 cy.ts파일을 나누었다.
- 허나 리팩토링을 진행하면서 두 파일의 모든 테스트 케이스가 통과 되는 것을 확인 하는 것이 매우 번거로웠음
- 시나리오 별로 나눌것인가 (O)
- 파일 내 테스트 케이스 구성을 아래와 같은 순서로 작성했다.
- 초기 페이지 UI 확인
- TextField의 유효성검사에 따른 HelperText 와 같은 Formik 확인
- 사용 시나리오에 따른 테스트 (API 유효성 검사 표함)
- 파일 내 테스트 케이스 구성을 아래와 같은 순서로 작성했다.
자 그럼 이제 구체적으로 테스트 코드를 어떤 식으로 작성했는지 알려드리겠음.
테스트 코드 작성 과정
1. 초기 페이지 UI 확인은 아주 간단하게 작성했다.
it('초기 페이지 UI 확인', () => {
cy.contains('센터 선택');
cy.contains('신청자(보호자)');
cy.contains('이용 여부 선택');
cy.contains('시설 이용자');
cy.dataCy('registerCenterSelect').should('exist');
// // TODO InputProps readOnly 테스트 작성
});
센터 선택이라는 글자가 포함되어 있는지, SelectBox가 존재하는지 등 매우 간단하게 작성했다.
2. UI 컴포넌트 테스트 시나리오는 ChatGPT 한테 물어보자.
이런 식으로 친절하게 테스트 케이스를 작성해 준다.
머리 싸매면서 어떤 경우를 테스트할지 고민하지 말고,
이런 걸 테스트하면 되겠구나~ 참고해서 테스트 케이스를 확보하자.
3. 특정 조건에 따라 변하는 경우에 컴포넌트 테스트를 작성하자.
<Select
data-cy={`registerrelation_${idx}`}
>
{values.availableType === 'admin' && <option value={0}>본인</option>}
<option value={1}> 자녀 </option>
</Select>
이런 식으로 특정 조건에서 SelectBox 내부가 변하는 경우에 작성해 주자.
it('[입장+ 시설이용] 라디오 버튼 클릭 후, [관계] Select Box UI 확인', () => {
cy.checkMuiRadioGroup('facilityRadioInRegisterStepOne', 'admin');
// Assert that the select box has "자녀" selected
cy.getMuiNativeSelect('registerrelation_0').eq(1).select('1');
cy.getMuiNativeSelect('registerrelation_0').eq(1).should('have.value', '1');
// Select "배우자"
cy.getMuiNativeSelect('registerrelation_0').eq(1).select('2');
cy.getMuiNativeSelect('registerrelation_0').eq(1).should('have.value', '2');
});
4. 작성하다 보면 해당 시나리오를 테스트하기 위한 사전 시나리오(?)가 중복되는 경우가 많다.
함수로 얼마만큼 묶을지는 본인이 판단. 함수로 일부 묶어도 여러번 반복됨.
함수를 묶어서 더 큰 함수를 만들어도 되기는 한데, 시나리오 절차를 한눈에 파악하고 싶어서 그러진 않았음.
5. 반복적으로 사용되는 Selector들을 초기에 변수화 시키자.
selector가 변경되었을 경우 실수를 줄일 수 있다.
describe('이용하기 > 파티/단체 예약', () => {
// 첫 번째 페이지
const goodsSelect = 'goodsInPartyQuestion';
function selectGoods() {
cy.getMuiNativeSelect(goodsSelect).eq(1).select('party');
}
it('예약문의 [첫 페이지] [상품] SelectBox UI 확인', () => {
cy.getMuiNativeSelect(goodsSelect).eq(1).select('group');
});
6. Formik 테스트 HelpText 작동 여부
it('예약문의 [두 번째] 페이지 [이름] HelperText UI 확인', () => {
nextOneStep();
cy.activateTextFieldFormik(nameTextField, stepTwoButton);
cy.equalHelpTextFormik(nameHelperText, ValidErrorsMsg.NAMEREQUIRED);
// 값 입력 이후 helpText 제거되는지 테스트
cy.removeHelpTextFormik(nameTextField, nameHelperText, '김민지');
// // 빈 값 테스트
cy.dataCy(nameTextField).should('exist').clear();
cy.equalHelpTextFormik(nameHelperText, ValidErrorsMsg.NAMEREQUIRED);
});
참고로 activateTextFieldFormik 은 내가 custom 하게 만든 command이다.
7. 시나리오는 경우의 수를 다양하게 작성
it('[입장 + 시설] 이용 등록 하는 경우 [자녀, 배우자] 2명 추가', () => {
...
});
it('[입장] 이용하는 경우 [삭제] 테스트', () => {
...
});
it('[입장] 이용하는 경우 [삭제] 이후 [추가]테스트', () => {
...
});
8. commands.ts를 이용해서 커스텀 명령어를 만들어 두자.
앱 같은 경우에는 로그인이 먼저 선행된 이후에 테스트가 가능하다. cy.ts 파일마다 로그인 함수를 작성할 순 없다.
이럴 때 이용하는 게 commands.ts이다.
commands.ts
declare namespace Cypress {
interface Chainable<Subject> {
login(email: string, password: string): Chainable<string>;
}
}
// -- This is a parent command --
Cypress.Commands.add('login', (email, password) => {
let logInformData = new FormData();
logInformData.append('id', email);
logInformData.append('password', password);
cy.request({
method: 'POST',
url: `${Cypress.env('rootUrl')}/login`,
body: { id: email, password: password},
}).then(({ body }) => {
localStorage.setItem('accessToken', body.token);
});
});
Register.cy.ts
describe('시설 이용 등록 페이지', () => {
beforeEach(() => {
const email = 'email';
const password = 'password';
cy.login(email, password);
});
it('초기 페이지 테스트', () => {});
});
만들어둔 cy.login을 사용할 수 있다.
특히 우리는 MUI를 사용하고 있기 때문에, selector를 이용할 때 nested 된 부분을 읽어올 필요가 있다.
declare namespace Cypress {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
login(email: string, password: string): Chainable<string>;
filledFindIdTextField(name: string, birth: string, phoneNum: string): Chainable<string>;
dataCy(value: string): Chainable<JQuery<HTMLElement>>;
// custom command for MUI
selectMuiSelect(selector: string, value: string);
getMuiNativeSelect(value: string): Chainable<JQuery<HTMLElement>>;
getMuiSelect(selector: string, value: string);
getMuiCheck(checkSelector): Chainable<JQuery<HTMLElement>>;
getTableRow(tbodySelector: string, trSelector: string): Chainable<JQuery<HTMLElement>>;
checkMuiRadioGroup(
radioGroupSelector: string,
radioControlSelector: string
): Chainable<JQuery<HTMLElement>>;
typeMuiTextField(selector: string, value: string): Chainable<JQuery<HTMLElement>>;
equalText(selector: string, value): Chainable<JQuery<HTMLElement>>;
// custom command for Formik
activateTextFieldFormik(
textSelector: string,
btnSelector: string
): Chainable<JQuery<HTMLElement>>;
removeHelpTextFormik(
selector: string,
helperTextSelector: string,
value: string
): Chainable<JQuery<HTMLElement>>;
equalHelpTextFormik(selector: string, msg: string): Chainable<JQuery<HTMLElement>>;
}
}
Cypress.Commands.add('activateTextFieldFormik', (textFieldSelector, submitBtnSelector) => {
cy.dataCy(textFieldSelector).should('exist').click();
cy.dataCy(submitBtnSelector).should('exist').click({ force: true });
});
Cypress.Commands.add('selectMuiSelect', (selector, value) => {
return cy.dataCy(`${selector}`).parent().click().get(`ul > li[data-value="${value}"]`).click();
});
Cypress.Commands.add('checkMuiRadioGroup', (radioGroupSelector, radioControlSelector) => {
return cy
.dataCy(`${radioGroupSelector}`)
.get(`label > span > input[value="${radioControlSelector}"]`)
.check();
});
Cypress.Commands.add('typeMuiTextField', (selector, value) => {
return cy.dataCy(`${selector}`).get(`div > input[value="${value}"]`).should('have.value', value);
});
MUI 전용을 만들어 두고 사용했다. 처음 작성할 땐 이 부분이 가장 오래 걸린다..
9. cypress.env.json 환경 변수를 이용하자.
cypress.env.json
{
"rootUrl": "https://localhost:8080/api/v1"
}
commands.ts
Cypress.env를 통해 사용할 수 있음
Cypress.Commands.add('login', (email, password) => {
let logInformData = new FormData();
logInformData.append('id', email);
logInformData.append('password', password);
cy.request({
method: 'POST',
url: `${Cypress.env('rootUrl')}/auth/app-login`,
body: { id: email, password: password },
}).then(({ body }) => {
localStorage.setItem('accessToken', body.token);
});
});
10. support 활용하기
E2E 테스트 코드를 작성하다 보면 페이지로 라우팅 해야 할 경우가 많다.
cy.visit를 이용해도 여러 번 중복될 가능성이 크고, 이는 결국 기존 로직을 수정하게 되었을 때 테스트 코드도 여러 개를 수정해야 하는 최악의 상황이 마주할 수 있다.
navigate.ts를 만들었다.
export class NavigatePage {
questionPage() {
cy.visit('/questions');
}
}
export const navigateCy = new NavigatePage();
Register.cy.ts
navigateCy. 함수()를 이용해서 사용한다.
describe('질문 페이지 테스트 코드', () => {
beforeEach(() => {
navigateCy.questionPage();
});
기존 코드에서 라우팅이 변경되었을 경우, 함께 챙겨줘야 하는 것은 동일 하지만.. 그래도 한 번만 수정하면 되기 때문에 어느 정도 관리의 비용이 줄진 않을까.. 싶다.
기타 꿀팁
배포 시
data-cy 굳이 필요 없음으로 제거하기
https://github.com/oliviertassinari/babel-plugin-react-remove-properties
{
"presets": [
...
],
// 추가
"env": {
"production": {
"plugins": [
[
"react-remove-properties",
{
"properties": ["data-cy"]
}
]
]
}
}
}
MUI 테스트
<Tabs/>, <TabPanel/> 테스트 방법
테스트 시나리오
기본은 다회권 탭이 클릭되어 있는 상태, 정기회원권 탭을 클릭할 경우
import Tabs from '@mui/material/Tabs';
import Tab from '@mui/material/Tab';
const [value, setValue] = React.useState<number>(0);
return (
<>
<Tabs
data-cy='premiumTabsInVipClub'
value={value}
onChange={handleChange}
>
<Tab label='다회권' />
<Tab label='정기 회원권' />
</Tabs>
<TabPanel
value={value}
index={0}/>
<TabPanel
value={value}
index={1}/>
</>
)
it('[정기회원권] 탭 클릭', () => {
cy.dataCy('premiumTabsInVipClub').type('1');
});
Tabs에서 Selector 만들어주고, type에 각 tab의 value를 넣어주면 됨.
느낀 점
테스트 코드 작성하며 이슈 수정 및 리팩토링을 해본 결과, 시간이 평소보다 두 배 정도 걸린다.
테스트 코드를 작성하는데 시간을 거의 다 쓴다. MUI Selector.. 불편 + 테스트 코드 작성에 익숙하지 않음
But 유지보수, 리팩토링을 하면서 (버튼 클릭해 보고 데이터를 입력하는) 테스트하는 시간은 절약할 수 있어서 좋다.
테스트가 실패할 때마다 Cypress를 의심하는데, 그러지 말고 나 자신을 의심하라.
굳이 필요 없는 테스트 코드는 과감히 지우자.
이슈가 발생할 만한 곳에 테스트 코드를 작성하자.
테스트 코드를 위한 리팩토링이 필요하다.
TODO
sorry-cypress
api mocking(cy-intercept)
(+추가)
BDD 형식으로 테스트 코드를 작성해보았다.
https://mingg123.tistory.com/195