Cypress 로 E2E 테스트 코드 작성하기 (vue, pinia)

물음표살인마·2025년 5월 27일
post-thumbnail

오늘의 작업

cypress e2e 테스트 코드를 작성해보았다. 내가 만든 페이지 중에서 트렌드 페이지에 진입하고 , 여성의류/심플베이직 버튼을 누르면 팝업이 뜨는 것, 팝업 내에서 카테고리를 선택했을 때의 액션, 스타일 태그를 선택했을 때의 액션을 테스트 코드로 작성을 했다. 아래 작업들을 어떻게 하면 되는지에 대해서 알아보겠다.

  • Cypress 테스트 코드 작성법
  • intercept로 api 모킹하기
  • 컴포넌트 식별자로 컴포넌트 선택하기
  • tsconfig.cypress.json
  • pinia store 가져오기

기본적인 구조

기본적인 구조를 살펴 보자면

//popupTest.cy.ts
describe('매장 스타일 관리', () => {
	beforeEach(() => {
     	 //같은 수준의 describe()나 context()블록 안의 
         //각각의 it() 테스트 케이스가 실행될 때마다 실행됩니다.
	});

	context('스토어 태그 버튼 클릭 시', () => {
		it('스토어 태그 버튼 클릭 시 팝업이 나타나야 한다', () => {
			//테스트 케이스
		});
		it('여성 의류 클릭 시 스타일 태그가 나타나야한다', () => {
			//테스트 케이스
		});
    });
});

이렇게 테스트 케이스를 작성한다.

context와 it

contextit으로 테스트 케이스를 나눈다. context는 테스트 그룹을 논리적으로 묶는 용도이고 “~한 상황에서” / “~일 때” 라는 의미단위를 가진다. 예를 들어 로그인한 사용자일때~ 이렇게 설정 할 수 있다. it은 테스트 케이스 단위이고 "~해야한다"라는 의미를 가진다. 그래서 context는 로그인한 사용자일때 it은 트렌드 화면이 보여야한다. 이렇게 테스트 케이스를 작성할 수 있다.

beforeEach()

beforeEach()는 같은 수준의 describe()나 context() 블록 안의 각각의 it() 테스트 케이스가 실행될 때마다 실행된다. it은 각각 독립적으로 실행되기 때문에 it이 실행되기 전에 먼저 선행되어야하는 작업을 beforeEach에 넣으면 된다.
우리 서비스에서는 로그인을 해야하기 때문에 beforeEach에 세션 관련 api를 모킹하여 응답을 받아오는 코드를 추가해주어야한다.

	beforeEach(() => {
		setupSession();
		setSomething([something]);
	});

api모킹(intercept)


const apiUrl = new URL(
	'/api/session',
	import.meta.env.VITE_ABARA_API_BASE
);

const TEST_ID = import.meta.env.VITE_CYPRESS_TEST_ID;
const TEST_PASSWORD = import.meta.env.VITE_CYPRESS_TEST_PASSWORD;

if (!TEST_ID || !TEST_PASSWORD) {
	throw new Error('계정 정보를 확인해주세요.');
}

function setupSession() {
	cy.clearAllCookies();
	cy.request('POST', apiUrl.toString(), {
		user: TEST_ID,
		password: TEST_PASSWORD,
	}).then((response) => {
		const {
			body: { content },
		} = response;

		cy.setCookie('access-token', content.accessToken);
	});
}
const url = new URL(PATH, import.meta.env.VITE_API_BASE);

export function setSomething(something: any[]) {
	cy.intercept(
		{ url: url.toString(), method: 'GET' },
		{
			statusCode: 200,
			body: {
				content: {
					something,
				},
				meta: {
					result: 'ok',
					code: 0,
					alertType: 0,
					resultMsg: '',
				},
			},
		}
	);
}

api를 모킹이 필요할 때는 intercept를 사용한다. intercept는 실제 서버로 요청을 보내지 않고 미리 정의한 응답을 돌려주는 역할을 한다.

컴포넌트 식별자 추가

	it('여성 의류 클릭 시 스타일 태그가 나타나야한다', () => {
			visitTrendsPage();
			clickStoreTagBtn();
			cy.get('[data-test="sale-selector"]')
				.contains('여성의류')
				.click();
			cy.contains('상품 스타일').should('be.visible');
		});
<selector
     data-test="sale-selector"
	... />

ui에서 테스트를 하려면 어떤 컴포넌트나 태그가 잘 선택되어 click이 되었는지, 화면에 나타났는지 확인하여야 한다. 이때 태그에 식별자를 넣어서 지정을 해주어야한다. 명칭은 이 문서를 참고하여 지정해주면 된다. 이렇게 해야 cy.get에서 정확한 컴포넌트가 선택이 된다. 그냥 태그명으로 하니까 선택이 안되더라.

나머지 cy함수들은 문서에서 보고 적절하게 사용하면 된다.

pinia store 가져오기

나는 더 나아가서 pinia 스토어 내부 함수를 가지고 오고 싶었다.

const useStyleTagSettingStore = defineStore('StoreTagSetting', () => {
	const { t } = useI18n();
  
	const handleSetPriority = (clickedItem: StyleTagItem) => {
    				...
    }
 });

pinia store코드는 위와 같은 코드였다.
일단 이것을 cy.ts 파일에 아래와 같이 import하려고 할때부터 타입스크립트 에러가 났다.

import useStyleTagSettingStore from '../../../src/store/useStyleTagSettingStore';

이 때는 tsconfig.cypress.json에 파일을 include해주어야한다.

{
	"extends": "@dd/tsconfig.json",
	"compilerOptions": {
		"composite": true,
		"types": ["cypress", "node"],
		"baseUrl": ".",
		"paths": {
			"@/*": ["src/*"],
		}
	},
	"include": [
		"tests/e2e/shims-vite.d.ts",
		"tests/e2e/**/*.ts",
		"src/store/useStyleTagSettingStore.ts",
	],
}

cypress전용 tsconfig에서는 include에 명시된 파일들만 타입 검사 대상으로 포함되기 때문에,그 외의 파일은 존재하지 않는 것처럼 취급되며, import 시 모듈을 찾지 못했다는 에러가 발생한다.

그렇게 import를 하는 것은 성공했는데 뜬금없이 vue cookie에서 문제가 생기는 것이다.

에러가 나는 이유는 내가 import 한 파일 내부의 내부를 타고 들어가면 cookie를 쓰고 있는데 런타임에 Cypress 환경에 없는 객체(예: Vue.$cookies, window, document 등)를 참조 하고 있기 때문이다. Cypress는 Node.js 환경에서 테스트 코드를 실행하기 때문에, 실제 브라우저/앱에서만 존재하는 전역 객체나 플러그인(예: Vue.$cookies)이 undefined가 된다.

해결방법은 문제가 되는 코드를 찾아가서 window나 cookie가 없을 경우 처리를 추가해주면 된다.

	if (typeof window !== 'undefined' && Vue?.$cookies) {}
   // or
	if (!Vue?.$cookies) return;

이제 import도 잘되고 에러도 나지 않는다.
그렇다면 test코드에 pinia 세팅하는 코드를 추가해주면 된다.

import { createPinia, setActivePinia } from 'pinia';

describe('매장 스타일 관리', () => {
	beforeEach(() => {
     	 //같은 수준의 describe()나 context()블록 안의 
         //각각의 it() 테스트 케이스가 실행될 때마다 실행됩니다.
	});

	context('스토어 태그 버튼 클릭 시', () => {
		it('스토어에서 함수 가져와서 사용하기', () => {
			setActivePinia(createPinia());
			const store = useStyleTagSettingStore();
          
          store.salesStyleDetail = [
				{
					attribute : '1'
				},
				{
					attribute : '2'
				},
            	{
					attribute : '3'
				},
			store.handleSetPriority(store.salesStyleDetail[0]);
			expect(store.salesStyleDetail[0].priority).to.equal(1);
		});
    });
});
	

예시 코드

import { createPinia, setActivePinia } from 'pinia';

import useStyleTagSettingStore from '../../../src/store/useStyleTagSettingStore';
//something import

const apiUrl = new URL(
	'/api/session',
	import.meta.env.VITE_ABARA_API_BASE
);

const TEST_ID = import.meta.env.VITE_CYPRESS_TEST_ID;
const TEST_PASSWORD = import.meta.env.VITE_CYPRESS_TEST_PASSWORD;
if (!TEST_ID || !TEST_PASSWORD) {
	throw new Error('계정 정보를 확인해주세요.');
}
function setupSession() {
	cy.clearAllCookies();
	cy.request('POST', apiUrl.toString(), {
		user: TEST_ID,
		password: TEST_PASSWORD,
	}).then((response) => {
		const {
			body: { content },
		} = response;

		cy.setCookie('access-token', content.accessToken);
		//...
	});
}

//중복 코드는 함수로 빼기
function visitTrendsPage() {
	// 트렌드 페이지로 이동
	cy.visit('/trends');
	// NewTrendInsights 컴포넌트가 로드될 때까지 대기
	cy.get('[data-test="something.."]').should('exist');
}

describe('매장 스타일 관리', () => {
	beforeEach(() => {
		setupSession();
	    setSomething(something);
	});

	context('스토어 태그 버튼 클릭 시', () => {
		it('스토어 태그 버튼 클릭 시 팝업이 나타나야 한다', () => {
			visitTrendsPage();
		});
		it('여성 의류 클릭 시 스타일 태그가 나타나야한다', () => {
			visitTrendsPage();
			cy.get('[data-test="something"]')
				.contains('여성의류')
				.click();
			cy.contains('상품 스타일').should('be.visible');
		});
	});

	context('여성의류 카테고리 선택 시 ', () => {
		it('스타일 태그를 클릭 시 순서대로 숫자가 표시 되어야한다 ', () => {
			//...
			cy.get('[data-test="something..."]')
				.should('have.length', 12)
				.then(($circles) => {
					// 1. 첫 번째 무작위 클릭
					const firstIndex = Math.floor(Math.random() * $circles.length);
					cy.wrap($circles[firstIndex]).click();
					cy.wrap($circles[firstIndex])
						.find('[data-test="something.."]')
						.contains('01');
					// 2. 두 번째(첫 번째와 다른 것) 무작위 클릭
					let secondIndex;
					do {
						secondIndex = Math.floor(Math.random() * $circles.length);
					} while (secondIndex === firstIndex);
					cy.wrap($circles[secondIndex]).click();
					cy.wrap($circles[secondIndex])
						.find('[data-test="something.."]')
						.contains('02');
					// 3. 세 번째(앞의 두 개와 다른 것) 무작위 클릭
					let thirdIndex;
					do {
						thirdIndex = Math.floor(Math.random() * $circles.length);
					} while (thirdIndex === firstIndex || thirdIndex === secondIndex);
					cy.wrap($circles[thirdIndex]).click();
					cy.wrap($circles[thirdIndex])
						.find('[data-test="something.."]')
						.contains('03');
				});
		});

		it('handleSetPriority 액션을 직접 호출하여 동작을 테스트한다', function () {
			// ...
			setActivePinia(createPinia());
			const store = useStyleTagSettingStore();
			store.salesStyleDetail = [
              {...}
			];
			store.handleSetPriority(store.salesStyleDetail[0]);
			expect(store.salesStyleDetail[0].priority).to.equal(1);

			//...
		});
	});
});

그러면 cypress가 돌아가면서 열심히 테스트를 실행한다.

profile
웹 프론트엔드 개발자

0개의 댓글