Next.js 환경에서 개발한 업로드 페이지에 Cypress를 이용해 E2E(End-to-End) 테스트를 구현했습니다.
이 테스트의 주요 목표는 업로드 페이지의 기본 렌더링부터 사진 등록, 장소 검색, 설명 입력, 그리고 최종적으로 등록 버튼이 활성화되는지 확인하는 전체 작업 흐름을 검증하는 것입니다.
테스트 코드를 적용하기 전 유저의 플로우를 예상해보고 정리했습니다.
describe('업로드 페이지의 기본 렌더링 및 입력과 버튼 검증 확인하기', () => {
it('사진 등록, 검색 모달창 입력, 설명 입력하면 등록 버튼이 활성화 된다.', () => {
cy.visit('/place/upload')
// 제목과 Form 렌더링 확인하기
cy.get('[data-cy="upload-title"]')
cy.get('form').should('be.visible')
// 사진 등록 시 미리보기가 보인다.
const imagePath = 'testimage.PNG' // cypress/fixtures에 정적 이미지 추가
cy.get('input[type="file"]').attachFile(imagePath)
cy.get('img').should('be.visible')
// 검색 모달창에서 입력하고 그 결과를 선택해서 업로드 폼에 다시 돌아온다.
cy.get('[data-cy="right-arrow-icon"]').click()
cy.get('[data-cy="search-modal"]').should('be.visible')
cy.get('[data-cy="search-modal-input"]').type('카페베네')
cy.get('[data-cy="search-modal-item"]').first().click()
cy.get('[data-cy="upload-form-input"]').should('have.value', '카페베네')
// 꿀플 노트 입력
cy.get('[data-cy="upload-form-textarea"]').type(
'정말 멋진 카페였습니다. 분위기도 좋고 커피도 맛있어요.'
)
// fetch 요청 모킹
cy.intercept('POST', '/api/create-notification', {
statusCode: 200,
body: { success: true },
}).as('createNotification')
// 등록 버튼이 활성화된다.
cy.get('[data-cy="upload-btn"]').should('not.be.disabled')
})
})
사용자가 다녀온 플레이스를 입력하기 위해 검색 모달창을 열고, 새로운 플레이스를 추가하는 과정을 테스트합니다.
다녀온 플레이스를 입력하려면 검색 모달창이 뜹니다.
그 이후 새로운 플레이스를 추가하면 newplace
와 map
페이지를 거쳐서 upload
페이지로 돌아옵니다.
이 테스트는 새로운 플레이스를 추가하는 복잡한 과정을 검증하기 위해 깊이가 3번 이상 되는 단계를 포함합니다.
단계별로 페이지가 이동하는지, 입력한 값이 올바르게 반영되는지를 확인하여 기능의 정상 동작을 보장합니다.
// describe 생략
it('모달창에서 새로운 꿀플레이스 추가하기를 누른 후 단계별 페이지 이동을 거쳐서 업로드 페이지로 돌아온다.', () => {
cy.visit('/place/upload')
// 모달창 띄우기
cy.get('[data-cy="right-arrow-icon"]').click()
cy.get('[data-cy="search-modal"]').should('be.visible')
cy.get('[data-cy="search-modal-input"]').type('새로운 가게')
cy.get('[data-cy="add-new-place-btn"]').click()
// 새로운 꿀플레이스 추가 페이지로 이동 확인
cy.url().should('include', '/place/newplace')
cy.get('[data-cy="newplace-name-input"]').should('have.value', '새로운 가게')
// 지도 페이지로 이동
cy.get('[data-cy="map-page-link"]').click()
cy.url().should('include', '/place/map')
// 지도 페이지에서 기본 주소 확인 및 위치 등록 (초기 주소값)
cy.get('p').contains('서울특별시 중구 세종대로 110').should('be.visible')
cy.get('[data-cy="map-btn"]').click()
// 새로운 꿀플레이스 추가 페이지로 돌아옴
cy.url().should('include', '/place/newplace')
cy.get('[data-cy="newplace-name-input"]').should('have.value', '새로운 가게')
cy.get('[data-cy="newplace-address-input"]').should(
'have.value',
'서울특별시 중구 세종대로 110'
)
// 업로드 페이지로 이동
cy.get('[data-cy="upload-page-link"]').click()
cy.url().should('include', '/place/upload')
// 업로드 폼에서 입력된 장소 확인
cy.get('[data-cy="upload-form-input"]').should('have.value', '새로운 가게')
})
기본 렌더링 확인
사진 등록 및 미리보기 확인
장소 검색 및 선택
설명 입력 및 등록 버튼 활성화
보통 Cypress 테스트를 작성할 때는 각 기능을 개별적으로 테스트하기 위해 여러 개의 it
블록으로 나눕니다.
예시코드
describe('카운터 앱', () => {
// 첫 번째 테스트 시나리오
it('페이지에 진입하면 카운터 앱이 정상적으로 실행된다(0이 표시된다)', () => {
cy.visit('http://localhost:3000');
cy.get('[data-cy=counter]').contains(0);
});
// 두 번째 테스트 시나리오
it('플러스 버튼을 누르면 카운터가 1이 증가한다', () => {
cy.visit('http://localhost:3000');
cy.get('[data-cy=add-button]').click();
cy.get('[data-cy=counter]').contains(1);
});
// 세 번째 테스트 시나리오
it('마이너스 버튼을 누르면 카운터가 1이 감소한다', () => {
cy.visit('http://localhost:3000');
cy.get('[data-cy=minus-button]').click();
cy.get('[data-cy=counter]').contains(-1);
});
});
본 업로드 페이지 테스트 코드는 전체 플로우를 검증하기 위해서 입력 상태가 유지되어야 하므로 하나의 it
블록에 통합되어 있습니다.
npm install --save-dev cypress
{
"scripts": {
"test": "cypress open"
}
}
터미널에서 명령어를 작성합니다.
npm test 또는
npm t
프로그램이 켜지면 E2E Testing을 선택하고 Chrome을 선택합니다.
cypress.config.ts와 cypress 폴더가 생성됩니다.
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
},
});
cypress.config.ts
에서baseUrl: 'http://localhost:3000'
를 추가하여 기본 URL을 설정합니다.upload.cy.ts
파일을 생성합니다.import 'cypress-file-upload'
describe('업로드 페이지의 기본 렌더링 및 입력과 버튼 검증 확인하기', () => {
it('사진 등록, 검색 모달창 입력, 설명 입력 후 등록 버튼을 누르면 홈으로 이동한다.', () => {
cy.visit('/place/upload')
// 제목과 Form 렌더링 확인하기
cy.get('[data-cy="upload-title"]')
cy.get('form').should('be.visible')
// 사진 등록 시 미리보기가 보인다.
const imagePath = 'testimage.PNG' // cypress/fixtures에 정적 이미지 추가
cy.get('input[type="file"]').attachFile(imagePath)
cy.get('img').should('be.visible')
// 검색 모달창에서 입력하고 그 결과를 선택해서 업로드 폼에 다시 돌아온다.
cy.get('[data-cy="right-arrow-icon"]').click()
cy.get('[data-cy="search-modal"]').should('be.visible')
cy.get('[data-cy="search-modal-input"]').type('카페베네')
cy.get('[data-cy="search-modal-item"]').first().click()
cy.get('[data-cy="upload-form-input"]').should('have.value', '카페베네')
// 꿀플 노트 입력
cy.get('[data-cy="upload-form-textarea"]').type(
'정말 멋진 카페였습니다. 분위기도 좋고 커피도 맛있어요.'
)
// fetch 요청 모킹
cy.intercept('POST', '/api/create-notification', {
statusCode: 200,
body: { success: true },
}).as('createNotification')
// 등록 버튼이 활성화된다.
cy.get('[data-cy="upload-btn"]').should('not.be.disabled')
})
})
<h2 data-cy='upload-title'>
cy.get('[data-cy="upload-title"]')
으로 요소를 가져옵니다.Cypress를 사용하여 파일 업로드를 테스트할 때 attachFile 메서드를 사용할 수 있습니다. 이 메서드는 cypress-file-upload 패키지를 통해 제공되며, 사용자가 파일을 업로드하는 시나리오를 자동화할 수 있게 해줍니다.
npm install --save-dev cypress-file-upload
패키지를 다운로드 합니다.
import 'cypress-file-upload';
테스트 코드 작성 상단에서 import 합니다.
const imagePath = 'testimage.PNG'
// cypress/fixtures 폴더에 위치한 파일
cy.get('input[type="file"]').attachFile(imagePath)
cy.get('img').should('be.visible')
파일 업로드를 테스트하기 위해 attachFile 메서드를 사용합니다. 이 메서드는 파일 인풋 요소에 파일을 첨부하는 기능을 제공합니다.
attachFile
메서드 설명테스트용 정적 이미지 등록: cypress/fixtures/testimage.PNG
파일을 등록하고 attachFile의 인수로 사용합니다.
메서드 사용: cy.get('input[type="file"]').attachFile(imagePath);는 파일 인풋 요소에 지정된 파일을 첨부합니다.
미리보기 확인: 파일이 첨부된 후, cy.get('img').should('be.visible');을 통해 업로드된 이미지의 미리보기가 정상적으로 표시되는지 확인합니다.
Firebase API 함수에 환경 변수를 사용하여 테스트 환경에서 실제 DB 호출을 피하는 방법입니다.
제 프로젝트의 업로드 페이지는 등록 버튼 시 로그인을 확인하기에 실제로 등록 버튼을 누르는 테스트는 생략하고, 모든 필드가 입력되었을 때 등록 버튼이 활성화 되는 상태만 확인했습니다.
npm install --save-dev cross-env
{
"scripts": {
"test": "cross-env NODE_ENV=test cypress open"
}
}
// firebase strage에 데이터를 등록하는 addHoneyPlace 함수
import { addDoc, collection } from 'firebase/firestore'
import { db } from '@root/firebase'
import { uploadNewPlace } from '@/interfaces/IPlace'
const isTestEnv = process.env.NODE_ENV === 'test'
export const addHoneyPlace = async (newPlace: uploadNewPlace) => {
if (isTestEnv) {
return
// 테스트 환경에서는 Firebase 호출 X
}
const placeDocRef = collection(db, 'honey_place')
return await addDoc(placeDocRef, newPlace)
}
------------------------------------
// 업로드 페이지의 등록 버튼 함수
const onSubmit: SubmitHandler<FieldValues> = async (formData) => {
const newPlace = {
name,
description,
address,
images: uploadedImageFiles,
createdAt: new Date(),
}
await addHoneyPlace(newPlace)
}
버튼을 못찾는 상황
button 태그가 아니라 만든 Button 컴포넌트여서.
Button 컴포넌트 수정:
Button 컴포넌트가 data-cy 속성을 받아 내부의 실제 HTML button 요소에 전달하도록 수정합니다.
// Button.tsx
interface ButtonProps {
type: 'button' | 'submit' | 'reset';
label: string;
disabled: boolean;
'data-cy'?: string; // data-cy 속성을 받을 수 있도록 추가
}
const Button: React.FC<ButtonProps> = ({ type, label, disabled, 'data-cy': dataCy }) => (
<button type={type} disabled={disabled} data-cy={dataCy}>
{label}
</button>
);
export default Button;
UploadForm 컴포넌트에서 Button 컴포넌트 사용:
UploadForm 컴포넌트에서 Button 컴포넌트를 사용할 때 data-cy 속성을 전달합니다.
tsx
코드 복사
<Button
data-cy='upload-btn'
type='submit'
label='꿀플레이스 로그 등록'
disabled={!isValid}
/>