안녕하세요. J4J입니다.
이번 포스팅은 Cypress Custom Command 사용하는 방법에 대해 적어보는 시간을 가져보려고 합니다.
Custom Command란?
Custom Command는 테스트 코드를 작성할 때 Cypress에서 제공해 주는 명령어가 아닌 사용자가 원하는 의도대로 동작될 수 있는 명령어를 자체적으로 구현한 것을 의미합니다.
Custom Command를 사용하는 대표적인 이유는 재사용성을 높이기 위함이라고 생각합니다.
테스트 코드를 작성하다 보면 중복되는 코드들이 수 없이 발생될 수 있습니다.
그럴 때마다 각 파일들에 중복 코드들을 매번 작성해주는 것은 비효율적이며 유지보수하기 어렵게 만들기 때문에 Custom Command를 생성한 뒤 단순 호출하는 방식을 선택하여 코드 개선을 할 수 있습니다.
JS 함수 사용
사실 테스트 코드가 아닌 일반 JS 코드들처럼 중복되는 소스를 하나의 함수로 만들어 재사용성을 동일하게 제공해 줄 수 있습니다.
예를 들어 상품을 등록하는 페이지에서 로그인을 위한 테스트 코드와, 자체적으로 커스텀한 textField의 input을 찾는 테스트 코드가 필요하다고 가정해 보겠습니다.
그리고 이들을 위한 소스는 다음과 같이 간단하게 구성해 봤습니다.
// /src/pages/login.tsx
import React from 'react';
import { useNavigate } from 'react-router';
const Login = () => {
/**
* navigate
*/
const navigate = useNavigate();
/**
* handle
*/
const handle = {
login: () => {
navigate('/home');
},
};
return (
<div>
<input data-cy="id-input" type="text" placeholder="ID" />
<input data-cy="password-input" type="password" placeholder="Password" />
<button data-cy="login-button" onClick={handle.login}>
로그인
</button>
</div>
);
};
export default Login;
// /src/components/molecules/textField.tsx
import React from 'react';
interface Props {
dataCy?: string;
}
const TextField = (props: Props) => {
return (
<div data-cy={props.dataCy}>
<input type="text" />
</div>
);
};
export default TextField;
// /src/pages/register.tsx
import React, { useState } from 'react';
import TextField from '../components/molecules/textField';
const Register = () => {
/**
* useState
*/
const [name, setName] = useState<string>('');
const [price, setPrice] = useState<number>(0);
return (
<div>
<input
data-cy="name-input"
type="text"
placeholder="상품명을 입력해주세요."
onChange={(e) => setName(e.target.value)}
/>
<input
data-cy="price-input"
type="number"
placeholder="가격을 입력해주세요."
onChange={(e) => setPrice(Number(e.target.value))}
/>
<TextField dataCy="textField-container" />
</div>
);
};
export default Register;
위와 같이 코드가 구성되어 있을 때 로그인과 textField의 input을 찾는 공통 코드는 다음과 같이 작성해 볼 수 있습니다.
// /cypress/support/commandFunction.ts
/**
* cypress login command
*/
export const login = () => {
// 페이지를 /login으로 이동
cy.visit('/login');
// id-input dom 요소에 '입력한 아이디'값을 입력
cy.get('[data-cy=id-input]').type('입력한 아이디');
// password-input dom 요소에 '1234'값을 입력
cy.get('[data-cy=password-input]').type('1234');
// login-button dom 요소가 존재하는지 확인 후 클릭 이벤트 발생
cy.get('[data-cy=login-button]').should('exist').click();
};
/**
* cypress get text field input dom
*/
export const getTextFieldInput = (dataCy: string) => {
return cy.get(`[data-cy=${dataCy}] > input`);
};
이 상태에서 다음의 시나리오대로 테스트를 작성해 보겠습니다.
- 로그인 수행
- 상품 등록 페이지 이동
- 상품명, 상품가격, 텍스트필드 값 입력
// /cypress/e2e/registerFunction.cy.ts
import { getTextFieldInput, login } from '../support/commandFunction';
describe('상품 등록 테스트', () => {
before(() => {
// 로그인
login();
});
it('상품 정보를 입력하기', () => {
// 페이지를 /register로 이동
cy.visit('/register');
// 이름 입력
cy.get('[data-cy=name-input]').type('테스트 상품명');
// 가격 입력
cy.get('[data-cy=price-input]').type('2000');
// 텍스트 필드 입력
getTextFieldInput('textField-container').type('텍스트 필드');
});
});
테스트 코드를 작성하고 테스트를 돌려보면 다음과 같은 결과를 확인해 볼 수 있습니다.
Custom Command 사용
위와 동일한 결과가 나오는 테스트를 Custom Command를 이용한 코드로 변경해 보겠습니다.
먼저 commandFunction.ts 대신 다음 코드로 대체해 줍니다.
// /cypress/support/commands.ts
/// <reference types="cypress" />
/**
* cypress login command
*/
Cypress.Commands.add('login', () => {
// 페이지를 /login으로 이동
cy.visit('/login');
// id-input dom 요소에 '입력한 아이디'값을 입력
cy.get('[data-cy=id-input]').type('입력한 아이디');
// password-input dom 요소에 '1234'값을 입력
cy.get('[data-cy=password-input]').type('1234');
// login-button dom 요소가 존재하는지 확인 후 클릭 이벤트 발생
cy.get('[data-cy=login-button]').should('exist').click();
});
/**
* cypress get text field input dom
*/
Cypress.Commands.add('getTextFieldInput', (dataCy: string) => {
cy.get(`[data-cy=${dataCy}] > input`);
});
그리고 타입 스크립트를 사용하시는 분들은 다음과 같이 선언한 명령어의 타입을 정의해줘야 합니다.
// cypress/support/cypress.d.ts
declare namespace Cypress {
interface Chainable {
login(): Chainable<JQuery<HTMLElement>>;
getTextFieldInput(dataCy: string): Chainable<JQuery<HTMLElement>>;
}
}
테스트 코드는 registerFunction.cy.ts대신 다음 코드로 대체해 줍니다.
여기서 주의할 점은 Custom Command가 정의된 파일을 import 해주는 것입니다.
(만약 /cypress/support/commands 파일에 정의했다면 import 해주지 않아도 됩니다.)
// /cypress/e2e/register.cy.ts
import '../support/commands';
describe('상품 등록 테스트', () => {
before(() => {
// 로그인
cy.login();
});
it('상품 정보를 입력하기', () => {
// 페이지를 /register로 이동
cy.visit('/register');
// 이름 입력
cy.get('[data-cy=name-input]').type('테스트 상품명');
// 가격 입력
cy.get('[data-cy=price-input]').type('2000');
// 텍스트 필드 입력
cy.getTextFieldInput('textField-container').type('텍스트 필드');
});
});
소스를 모두 변경하고 register.cy.ts에 대한 테스트를 돌려보면 다음과 같은 결과를 확인할 수 있습니다.
결론 + 부가 정보
JS 함수를 사용할 때와 Custom Command를 사용하는 것을 비교해 보면 아직 사용 경험이 부족해서인지 모르겠지만 "어떤 것이 더 좋으니 이걸 사용해야겠다"라는 것을 느낄 수 없었습니다.
JS 함수를 사용하게 될 경우 테스트 코드가 아닌 소스들을 작성하는 것처럼 활용될 수 있다는 것을 확인할 수 있었고 반대로 Custom Command를 사용하게 될 경우 Cypress에서 제공해 주는 명령어처럼 활용될 수 있다는 것을 볼 수 있었습니다.
즉, 무엇이 더 좋다고 말할 수 없기에 같이 일하는 개발자분들이 어떤 코드 스타일을 더 선호하는지에 따라 사용 방법이 달라질 수 있을 것 같다고 느낄 수 있었습니다.
Cypress 공식 문서를 확인해 보면 Custom Command를 사용하는 방향은 두 가지가 있습니다.
- Cypress.Commands.add() → 신규 명령어를 추가
- Cypress.Commands.overwrite() → Cypress에서 제공해 주는 명령어를 재 정의
개인적인 생각으론 overwrite보단 add를 사용하는 것이 좋을 것 같다고 느껴집니다.
왜냐하면 add가 overwrite로 수행하려는 역할을 대체해 줄 수 있기도 하고 또한 Cypress에서 제공해주는 것처럼 동작하지 않으면 함께 개발할 때 혼란의 여지를 만들 수 있다고 생각하기 때문입니다.
마지막으로 공식 문서를 한번 더 확인해 보면 Custom Command의 쿼리는 /cypress/support/commands.js 파일에 정의되는 것을 추천하고 있습니다.
이유는 다음과 같습니다.
We recommend defining queries is in your cypress/support/commands.js file, since it is loaded before any test files are evaluated via an import statement in the supportFile.
이상으로 Cypress Custom Command 사용하는 방법에 대해 간단하게 알아보는 시간이었습니다.
읽어주셔서 감사합니다.
'SPA > React' 카테고리의 다른 글
[React] Tailwind 사용하기 (2) - StyledComponents와 CSS 적용 비교하기 (0) | 2023.05.22 |
---|---|
[React] Tailwind 사용하기 (1) - 개념 및 설정 (0) | 2023.05.17 |
[React] Cypress API 요청 기다리기 (0) | 2023.04.23 |
[React] Cypress로 내부 DOM 테스트하기 (0) | 2023.04.16 |
[React] Cypress로 E2E테스트하기 (0) | 2023.04.12 |
댓글