[React] Cypress๋ก e2e ํ ์คํธ๋ฅผ ๊ตฌํํด๋ณด์
Next.js๋ฅผ ํฌํจํ React ํ๋ก์ ํธ์์ Cypress๋ก e2e๋ฅผ ๊ตฌํํด ๋ด ์๋ค.
ํ๋ก์ ํธ์ Cypress๋ฅผ ์ค์นํด ์ค๋๋ค.
$ yarn add cypress --dev
or
$ npm install cypress --save-dev
Package.json์ ๊ฐ์ ์คํฌ๋ฆฝํธ๋ฅผ ์ถ๊ฐํด ์ค๋๋ค.
"scripts": {
...,
"test": "cypress open",
"cy:run": "cypress run"
},
ํฐ๋ฏธ๋์์ ๋ช ๋ น์ด๋ฅผ ์์ฑํด ๋ด ์๋ค
$ yarn test
or
$ npm run test
๊ทธ๋ผ ์๋์ ๊ฐ์ ํ๋ก๊ทธ๋จ์ด ์ผ์ง ๊ฒ๋๋ค!
์ฐ๋ฆฌ๋ e2e๋ฅผ ๊ตฌํํ ๊ฒ์ด๋๊น ์ฒซ ๋ฒ์งธ๋ฅผ ์ ํํ๊ณ ํ์ผ์ ์์ฑํด ์ค๋ค๋ ๊ฑฐ์ ๋์ํ๊ณ ๋์ด๊ฐ๋๋ค.
์ ๊ธฐ์ค Electron์ด ํธํด์ ์ ํํ์ฌ Start๋ฅผ ๋๋ฆ ๋๋ค.
โ๏ธ ์ด ๊ณผ์ ์์ ํฌ๋กฌ์ด๋ Electron์ ์ผ๋ฉด ๋น ํ๋ฉด(blank screen)์ด ๋จ๋ ๊ฒฝ์ฐ๊ฐ ์๋๋ฐ
์ฐธ๊ณ (https://github.com/cypress-io/cypress/issues/4131) ์ ๊ธ์์๋ ์น์์ผ ๋ฌธ์ , ํ๋ฌ๊ทธ์ธ ๋ฌธ์ ๋ฑ๋ฑ์ ์ธ๊ธํฉ๋๋ค.
์ ๋ ๊ณต์๋ฌธ์๋ง ๋ณด๊ณ ๋ฐ๋ผ ํ์ ๋ฟ์ธ๋ฐ ์ ๋ผ์ ๊ทผ๋ณธ์ ์ธ ํด๊ฒฐ ๋ฐฉ๋ฒ์ด ์๋๋ผ ์๊ฐํ์ต๋๋ค.
yarn test๋ฅผ ์คํํ ํฐ๋ฏธ๋์์ ์ค๋ฅ๊ฐ ํ๋๋ ์๋๋ฐ ํด๋น ํ์์ด ๊ณ์ ์ง์๋์์ง๋ง, ๋๋๊ฒ๋ ๋งฅ๋ถ ์ฌ๋ถํ ํ๋๊น ํด๊ฒฐ์ด ๋์ต๋๋ค.
โ ์ปดํจํฐ ์ฌ๋ถํ ์ ํ ๋ฒ ํด๋ณด์ธ์!
๋ค์ ์ฝ๋๋ก ๋์๊ฐ๋ด ์๋ค.
cypress.config.ts์ ์ ์ญ ํด๋์ cypress๊ฐ ์์ฑ๋์์ ๊ฒ๋๋ค.
์ ๋ ์๋์ ๊ฐ์ด ์์ฑํ์ต๋๋ค.
// cypress.config.ts
import { defineConfig } from 'cypress';
export default defineConfig({
projectId: process.env.CYPRESS_PROJECT_ID,
e2e: {
baseUrl: 'http://localhost:3000',
setupNodeEvents(on, config) {
// implement node event listeners here
}
},
chromeWebSecurity: false
});
cypress ํด๋ ๋ด๋ถ์ e2e ํด๋๋ฅผ ํ๋ ์์ฑํด์ฃผ์ธ์.
e2e ํด๋์์ ๊ฐ์ ํ ์คํธ ์ผ์ด์ค๋ค์ ์ง์ ํด ์ฃผ๋ฉด ๋ฉ๋๋ค.
// cypress/e2e/signIn.cy.ts
describe('SignIn', () => {
beforeEach(() => {
cy.visit('/auth/signIn');
});
it('should signIn successfully', () => {
cy.get('[type="text"]').type('test@test.com');
cy.get('[type="password"]').type('testUser123');
cy.contains('๋ก๊ทธ์ธ').click();
cy.url().should('include', '/main');
});
it('should navigate to sign up page successfully', () => {
cy.contains('ํ์๊ฐ์
').click();
cy.url().should('include', '/auth/signUp');
});
});
๊ฐ๋จํ๊ฒ ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํด ๋ณด์์ต๋๋ค.
cypress.config.ts์ baseUrl์ ์ง์ ํด ์ค์ pathname๋ง ์ง์ ํด ์ค๋ ๋์ํฉ๋๋ค.
beforeEach() ๋ด๋ถ์ ์ ์ํ ๊ฑด ํด๋น describe์ ์ ์๋ ํ ์คํธ ์ผ์ด์ค๋ค์ด ๋ชจ๋ ๊ณต์ ํ๋ค๊ณ ์๊ฐํ๋ฉด ๋ฉ๋๋ค.
์ฆ ๋ชจ๋ ์์ํ ๋ /auth/signIn์ ๋ฐฉ๋ฌธํ๊ฒ ์ฃ ?
ํด๋น ์ฝ๋๋ ์๋์ ๊ฐ์ด ์ค๋ณต ์ฝ๋๋ฅผ ์ค์ด๋ ๋ฐ ๋์์ด ๋ฉ๋๋ค.
describe('invalid inputs', () => {
beforeEach(() => {
cy.get('[id="nickname"]').type('test');
cy.get('[id="email"]').type('test@naver.com');
cy.get('[id="password"]').type('testUser123');
cy.get('[id="password_check"]').type('testUser123');
});
it('should display an error message when nickname is too short', () => {
cy.get('[id="nickname"]').clear().type('h');
cy.contains('๊ฐ์
ํ๊ธฐ').should('be.disabled');
});
it('should display an error message when email is invalid', () => {
cy.get('[id="email"]').clear().type('testnaver.com');
cy.contains('๊ฐ์
ํ๊ธฐ').should('be.disabled');
});
it('should display an error message when password is too short', () => {
cy.get('[id="password"]').clear().type('1234');
cy.contains('๊ฐ์
ํ๊ธฐ').should('be.disabled');
});
it('should display an error message when match check password is not same', () => {
cy.get('[id="password_check"]').clear().type('testUser124');
cy.contains('๊ฐ์
ํ๊ธฐ').should('be.disabled');
});
});
๋ณธ์ธ์ด ํ์ํ ํ ์คํธ ์ผ์ด์ค๋ฅผ ์์ฑํ๊ณ ๋ค์ Cypress ํ๋ก๊ทธ๋จ์ผ๋ก ๊ฐ๋ณด๋ฉด
์๋์ ๊ฐ์ด E2E specs์ ์ ์ํ ํ ์คํธ๋ค์ด ์ ์๋์ด ์์ ๊ฑฐ์์.
์ฌ๊ธฐ์ ํด๋ฆญํ๋ฉด ์๋ ์์์ฒ๋ผ GUI๋ก ๋น๋์ค ๋ฐ ์ค๋ ์์ ๋ณด๋ฉด์ ๋ฐ๋ก ํ์ธํ ์ ์์ต๋๋ค.
CLI๋ก ํ๋ ค๋ฉด ์์์ ์ ์ํ cy:run ์คํฌ๋ฆฝํธ๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฉ๋๋ค.
$ yarn run cy:run -- --record --spec "cypress/e2e";
or
$ npm run cy:run -- --record --spec "cypress/e2e";
์ ๋ช ๋ น์ด๋ฅผ ์์ฑํ๋ฉด e2e ๋ด๋ถ์ ์๋ ๋ชจ๋ ํ ์คํธ๋ค์ ์๋ํ์ฌ ํฐ๋ฏธ๋์ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํด ์ค๋๋ค.
๊ฐ์ธ์ ์ผ๋ก Cypress ํ๋ก๊ทธ๋จ์์ ์์ฑํ๋ ๊ฒ๋ณด๋จ ์ฝ๋๋ก ์์ฑํ๋ ๊ฒ ์ข์ ๊ฒ ๊ฐ์ต๋๋ค.
CLI์์ ํ ์คํธ๋ฅผ ์๋ํ๋ฉด Cypress์์ screenshots, videos ํด๋์ ๋ฐ์ดํฐ๋ค์ด ์์ด๋๋ฐ ๊นํ๋ธ์ ์ฌ๋ผ๊ฐ์ง ์๋๋ก
. gitignore์ ์ถ๊ฐํด ์ค์๋ค
# cypress video, screenshots
cypress/videos
cypress/screenshots
์๋ PR์ e2e ๊ด๋ จ ์ฝ๋๋ฅผ ์ ์ด๋์ ์ฐธ๊ณ ํ๋ฉด ์ข์ ๊ฒ ๊ฐ์ต๋๋ค.
https://github.com/Nimble-Meet/client/pull/28