라이브러리 제작에서 사용한 E2E Test Cypress
fetch 라이브러리의 안정성을 확보하기 위해 사용했던 Cypress에 대해 공유하고자 합니다.
다양한 테스트 방법이 있지만, 인증 관리가 필요한 이 라이브러리 테스트에 있어 Cypress를 활용한 E2E 테스트를 선택한 이유는 다음과 같습니다.
일반적으로 테스트 방법에는 다음과 같은 유형이 있습니다
- 단위 테스트: 함수나 메서드와 같은 코드의 개별적인 부분을 독립적으로 검증하는 테스트입니다. 빠른 피드백을 제공하며, 비즈니스 로직이 올바르게 동작하는지를 주로 확인합니다.
- 통합 테스트: 여러 모듈이 서로 상호작용하는 방식을 테스트하여, 시스템의 각 부분이 통합되어 제대로 작동하는지를 확인합니다. 종속성이나 모듈 간의 상호작용을 검증하기에 유용합니다.
- E2E 테스트: 실제 사용 환경을 재현하여 전체적인 사용자 흐름을 테스트합니다. UI와 API의 상호작용, 인증과 같은 사용자 경험을 통합적으로 검증하기 때문에 최종 사용자에게 가까운 방식으로 테스트할 수 있습니다.
이번 프로젝트에서는 인증 흐름을 포함한 복잡한 사용자 시나리오를 검증해야 했기 때문에, Cypress를 사용하여 E2E 테스트를 수행했습니다. Cypress를 선택한 이유는 다음과 같습니다
- 인증 흐름 자동화: Cypress는 브라우저 환경을 완벽하게 재현할 수 있어, 로그인, 토큰 발급 및 갱신, 세션 유지 등 인증이 필요한 모든 과정을 사용자처럼 자동화하여 테스트할 수 있었습니다.
- 상태 관리: Cypress는 쿠키와 로컬 스토리지와 같은 상태를 관리할 수 있어, 인증 상태에 따른 접근 제어와 세션 유지 등의 시나리오를 테스트하기에 적합했습니다.
- 실제 사용자 경험에 가까운 시나리오 구성: Cypress의 명령 체인을 통해 여러 API 호출과 토큰 갱신 과정을 순차적으로 테스트하여 실제 사용 흐름을 재현할 수 있었습니다.
Cypress 설치
먼저, Cypress를 설치하여 프로젝트에서 사용할 수 있도록 설정합니다.
npm으로 설치
npm install cypress --save-dev
yarn으로 설치
yarn add cypress --dev
pnpm으로 설치
pnpm add cypress --save-dev
Cypress 설정 파일 구성 및 역할 설명
Cypress를 사용하여 E2E 테스트를 진행하면서, 저는 여러 설정 파일을 커스터마이징하여 테스트 효율성을 높였습니다. 각 파일이 어떤 역할을 하는지 설명하겠습니다.
1. command.ts: 커스텀 명령어 정의
command.ts 로그인 관련 작업을 cy.login 명령어로 만들어 테스트 전반에 쉽게 재사용할 수 있도록 했습니다.
/// <reference types="cypress" />
const SUPABASE_URL = Cypress.env("SUPABASE_URL");
const SUPABASE_ANON_KEY = Cypress.env("SUPABASE_ANON_KEY");
// Add login command
Cypress.Commands.add("login", (email: string, password: string) => {
cy.request({
method: "POST",
url: `${SUPABASE_URL}/auth/v1/token?grant_type=password`,
body: { email, password },
headers: {
apikey: SUPABASE_ANON_KEY,
"Content-Type": "application/json",
},
failOnStatusCode: false,
}).then((response) => {
if (response.status === 200) {
Cypress.env("ACCESS_TOKEN", response.body.access_token);
Cypress.env("REFRESH_TOKEN", response.body.refresh_token);
} else {
throw new Error("Login failed: " + response.body.message);
}
});
});
cy.login 명령어를 통해 로그인 요청을 보내고, 성공 시 발급받은 ACCESS_TOKEN과 REFRESH_TOKEN을 환경 변수에 저장하여 이후 테스트에서 인증 상태를 유지할 수 있습니다.
2. index.ts: 타입 정의 및 전역 설정
index.ts 파일에서는 Cypress의 전역 설정을 관리하고, 커스텀 명령어에 대한 타입 정의를 추가했습니다. TypeScript는 Cypress 커스텀 명령어를 기본적으로 인식하지 않기 때문에, cy.login 명령어를 TypeScript에서도 사용할 수 있도록 타입을 선언했습니다.
declare namespace Cypress {
interface Chainable<Subject = any> {
/**
* Custom command to log in to Supabase
* @example cy.login('email', 'password')
*/
login(email: string, password: string): Chainable<void>;
}
}
3. e2e.ts: API 인스턴스 생성
e2e.ts 파일은 Cypress가 매 E2E 테스트 실행 전에 자동으로 로드하는 설정 파일입니다. 이 파일은 커스텀 명령어 등록, 글로벌 설정 및 예외 처리 등을 통해 테스트의 안정성과 편의성을 높여줍니다.
테스트 실행 전 설정
before(() => {
// Example: Log in test user or perform other initial setup
cy.login("test1234@test.com", "123456");
});
before 훅을 통한 전역 설정: 모든 테스트가 시작되기 전에 실행될 설정을 정의합니다. 예제에서는 cy.login을 호출하여 특정 계정(test1234@test.com)으로 로그인한 상태에서 테스트가 시작되도록 설정했습니다. 이를 통해 매 테스트마다 로그인을 반복할 필요 없이, 필요한 초기 설정을 한 번만 적용할 수 있습니다.
전역 예외 처리
전역 예외 처리: Cypress는 기본적으로 uncaught:exception 이벤트를 발생시키면 테스트를 중단하지만, 여기서는 모든 예외를 무시하고 테스트가 중단되지 않도록 설정했습니다
Cypress.on("uncaught:exception", (err, runnable) => {
// Ignore the error and continue the tests
return false;
});
4.test.cy.ts
Cypress 테스트 구조
describedescribe는 테스트를 그룹화하는 데 사용됩니다. 테스트 모음에 대해 설명을 제공하며, 이를 통해 테스트를 논리적 단위로 구성할 수 있습니다.
itit은 단일 테스트 케이스를 정의하는 데 사용됩니다. describe 내부에 여러 개의 it을 포함하여 다양한 테스트를 실행할 수 있습니다.
expectexpect는 테스트의 예상 결과를 정의하는 Assertion입니다. 이를 통해 테스트 결과가 예상한 대로 동작하는지 확인할 수 있습니다.
먼저 Api 클래스의 인스턴스를 생성합니다. 이 인스턴스는 baseUrl, getToken, onRefreshToken과 같은 속성을 설정하여 Supabase와의 API 통신을 처리합니다. getToken은 요청 시 토큰을 가져오는 함수이고, onRefreshToken은 토큰 만료 시 새로운 토큰을 발급받는 함수입니다.
인스턴스 생성
const api = new Api({
baseUrl: SUPABASE_URL, // API 기본 URL
getToken: () => Cypress.env("ACCESS_TOKEN"), // API 요청에 필요한 토큰
onRefreshToken: async () => {
// 토큰 갱신 처리
const { data, error } = await supabase.auth.refreshSession({
refresh_token: Cypress.env("REFRESH_TOKEN"),
});
if (error) {
throw new Error("Token refresh failed: " + error.message);
}
if (data?.session?.access_token) {
Cypress.env("ACCESS_TOKEN", data.session.access_token);
Cypress.env("REFRESH_TOKEN", data.session.refresh_token);
cy.log("New ACCESS_TOKEN:", data.session.access_token);
cy.log("New REFRESH_TOKEN:", data.session.refresh_token);
} else {
throw new Error("Failed to get a new access token");
}
},
});
요청 보내기
이 Api 인스턴스를 사용하여 Supabase API에 GET 요청을 보냅니다. 요청에 필요한 ACCESS_TOKEN과 apikey를 beforeRequest에서 설정하고, 성공 시 응답 데이터를 검증합니다.
api.get({
url: `${API_ENDPOINT}/test`,
query: { select: "*" },
beforeRequest: (url, options) => {
options.headers = {
...options.headers,
apikey: SUPABASE_ANON_KEY,
};
cy.log("Using ACCESS_TOKEN:", Cypress.env("ACCESS_TOKEN"));
},
onSuccess: (data) => {
const responseData = data as any[];
expect(responseData).to.be.an("array");
if (responseData.length > 0) {
expect(responseData[0]).to.have.property("key");
}
},
onError: (error) => {
throw new Error("GET request failed: " + error.message);
},
});