Cypress 치트시트
Cypress - End-to-End Testing Made Easy
Cypress는 현대 웹을 위해 만들어진 차세대 프론트엔드 테스팅 도구입니다. 개발자와 QA 엔지니어가 현대 애플리케이션을 테스트할 때 직면하는 주요 문제점들을 해결합니다. Cypress를 사용하면 모든 유형의 테스트를 작성할 수 있습니다: 엔드투엔드 테스트, 통합 테스트, 단위 테스트.
(No text to translate)4-20. (Since no specific text was provided for these sections, I cannot translate them. If you provide the specific text, I’ll be happy to translate it.)
Would you like me to translate the Table of Contents section as well? If so, here’s a translation:
목차
Would you like me to proceed with the rest of the translation or do you want to provide specific text for sections 4-20?```bash
Install Cypress via npm
npm install —save-dev cypress
Install Cypress via yarn
yarn add —dev cypress
Open Cypress for the first time
npx cypress open
Run Cypress in headless mode
npx cypress run
### Project Setup
```bash
# Initialize new project with Cypress
mkdir my-cypress-project
cd my-cypress-project
npm init -y
npm install --save-dev cypress
# Open Cypress (creates folder structure)
npx cypress open
Folder Structure
cypress/
├── e2e/ # End-to-end test files
│ └── spec.cy.js
├── fixtures/ # Test data
│ └── example.json
├── support/ # Support files
│ ├── commands.js # Custom commands
│ └── e2e.js # Global configuration
└── downloads/ # Downloaded files during tests
Package.json Scripts
{
"scripts": {
"cypress:open": "cypress open",
"cypress:run": "cypress run",
"cypress:run:chrome": "cypress run --browser chrome",
"cypress:run:firefox": "cypress run --browser firefox",
"cypress:run:edge": "cypress run --browser edge",
"cypress:run:headed": "cypress run --headed",
"cypress:run:record": "cypress run --record --key=your-record-key"
}
}
Getting Started
First Test
// cypress/e2e/first-test.cy.js
describe('My First Test', () => {
it('Visits the Kitchen Sink', () => {
cy.visit('https://example.cypress.io')
cy.contains('type').click()
cy.url().should('include', '/commands/actions')
cy.get('.action-email')
.type('fake@email.com')
.should('have.value', 'fake@email.com')
})
})
Basic Test Structure
describe('Test Suite Name', () => {
beforeEach(() => {
// Runs before each test
cy.visit('/login')
})
it('should do something', () => {
// Test implementation
cy.get('[data-cy=username]').type('user@example.com')
cy.get('[data-cy=password]').type('password123')
cy.get('[data-cy=submit]').click()
cy.url().should('include', '/dashboard')
})
it('should do something else', () => {
// Another test
})
})
Test Hooks
describe('Hooks Example', () => {
before(() => {
// Runs once before all tests in the block
cy.task('seedDatabase')
})
after(() => {
// Runs once after all tests in the block
cy.task('clearDatabase')
})
beforeEach(() => {
// Runs before each test in the block
cy.login('user@example.com', 'password123')
})
afterEach(() => {
// Runs after each test in the block
cy.clearCookies()
cy.clearLocalStorage()
})
it('should test something', () => {
// Test implementation
})
})
Basic Commands
Navigation
// Visit a URL
cy.visit('https://example.com')
cy.visit('/relative-path')
cy.visit('/', { timeout: 30000 })
// Navigate browser history
cy.go('back')
cy.go('forward')
cy.go(-1) // Go back one page
cy.go(1) // Go forward one page
// Reload page
cy.reload()
cy.reload(true) // Force reload
Element Interaction
// Click elements
cy.get('button').click()
cy.get('button').click({ force: true }) // Force click
cy.get('button').dblclick() // Double click
cy.get('button').rightclick() // Right click
// Type text
cy.get('input').type('Hello World')
cy.get('input').type('user@example.com{enter}')
cy.get('input').type('{selectall}New Text')
cy.get('input').clear().type('New Text')
// Select options
cy.get('select').select('Option 1')
cy.get('select').select(['Option 1', 'Option 2']) // Multiple
cy.get('select').select('value', { force: true })
// Check/uncheck
cy.get('input[type="checkbox"]').check()
cy.get('input[type="checkbox"]').uncheck()
cy.get('input[type="radio"]').check()
// File upload
cy.get('input[type="file"]').selectFile('path/to/file.pdf')
cy.get('input[type="file"]').selectFile(['file1.jpg', 'file2.jpg'])
Element Queries
// Get elements
cy.get('.class-name')
cy.get('#id')
cy.get('[data-cy="element"]')
cy.get('button').first()
cy.get('button').last()
cy.get('button').eq(2) // Third button (0-indexed)
// Find within elements
cy.get('.parent').find('.child')
cy.get('.parent').within(() => {
cy.get('.child').click()
})
// Filter elements
cy.get('button').filter('.active')
cy.get('button').not('.disabled')
// Contains text
cy.contains('Submit')
cy.contains('button', 'Submit')
cy.get('button').contains('Submit')
Traversal
// Parent/child relationships
cy.get('.child').parent()
cy.get('.element').parents('.ancestor')
cy.get('.parent').children()
cy.get('.parent').children('.specific-child')
// Siblings
cy.get('.element').siblings()
cy.get('.element').next()
cy.get('.element').prev()
cy.get('.element').nextAll()
cy.get('.element').prevAll()
// Closest
cy.get('.element').closest('.container')
Waiting
// Wait for element
cy.get('.loading').should('not.exist')
cy.get('.content').should('be.visible')
// Wait for specific time (not recommended)
cy.wait(1000)
// Wait for network requests
cy.intercept('GET', '/api/users').as('getUsers')
cy.wait('@getUsers')
// Wait for multiple requests
cy.wait(['@getUsers', '@getPosts'])
Selectors
CSS Selectors
// Basic selectors
cy.get('button') // Tag
cy.get('.btn') // Class
cy.get('#submit') // ID
cy.get('[type="submit"]') // Attribute
// Combinators
cy.get('form button') // Descendant
cy.get('form > button') // Direct child
cy.get('label + input') // Adjacent sibling
cy.get('h2 ~ p') // General sibling
// Pseudo-selectors
cy.get('button:first')
cy.get('button:last')
cy.get('button:nth-child(2)')
cy.get('input:checked')
cy.get('input:disabled')
cy.get('a:contains("Click me")')
Data Attributes (Recommended)
// HTML
// <button data-cy="submit-btn">Submit</button>
// <input data-testid="username" />
// <div data-test="user-profile">...</div>
// Cypress tests
cy.get('[data-cy="submit-btn"]')
cy.get('[data-testid="username"]')
cy.get('[data-test="user-profile"]')
// Custom command for data-cy
Cypress.Commands.add('dataCy', (value) => {
return cy.get(`[data-cy=${value}]`)
})
// Usage
cy.dataCy('submit-btn').click()
XPath (with plugin)
# Install cypress-xpath plugin
npm install --save-dev cypress-xpath
// cypress/support/e2e.js
require('cypress-xpath')
// Usage in tests
cy.xpath('//button[contains(text(), "Submit")]')
cy.xpath('//input[@placeholder="Username"]')
cy.xpath('//div[@class="container"]//button[1]')
Complex Selectors
// Multiple attributes
cy.get('input[type="email"][required]')
// Partial attribute matching
cy.get('[class*="btn"]') // Contains
cy.get('[class^="btn"]') // Starts with
cy.get('[class$="primary"]') // Ends with
// Multiple selectors
cy.get('button, input[type="submit"]')
// Chaining selectors
cy.get('.form-group')
.find('input')
.filter('[required]')
.first()
Assertions
Should Assertions
// Existence
cy.get('button').should('exist')
cy.get('.loading').should('not.exist')
// Visibility
cy.get('button').should('be.visible')
cy.get('.modal').should('not.be.visible')
// Text content
cy.get('h1').should('contain', 'Welcome')
cy.get('h1').should('have.text', 'Welcome to our site')
cy.get('p').should('include.text', 'partial text')
// Attributes
cy.get('input').should('have.attr', 'placeholder', 'Enter email')
cy.get('button').should('have.class', 'btn-primary')
cy.get('input').should('have.value', 'test@example.com')
// CSS properties
cy.get('button').should('have.css', 'background-color', 'rgb(0, 123, 255)')
cy.get('div').should('have.css', 'display', 'none')
// Length
cy.get('li').should('have.length', 5)
cy.get('li').should('have.length.greaterThan', 3)
cy.get('li').should('have.length.lessThan', 10)
Expect Assertions
// Using expect (Chai)
cy.get('input').then(($input) => {
expect($input).to.have.value('expected value')
expect($input[0].validity.valid).to.be.true
})
// Multiple assertions
cy.get('button').then(($btn) => {
expect($btn).to.have.class('btn')
expect($btn).to.contain('Submit')
expect($btn).to.be.visible
})
// Custom assertions
cy.get('.user-list').then(($list) => {
const users = $list.find('.user-item')
expect(users).to.have.length.above(0)
expect(users.first()).to.contain('John Doe')
})
URL Assertions
// URL should include
cy.url().should('include', '/dashboard')
cy.url().should('eq', 'https://example.com/dashboard')
// URL should match pattern
cy.url().should('match', /\/users\/\d+/)
// Location assertions
cy.location('pathname').should('eq', '/dashboard')
cy.location('search').should('eq', '?tab=profile')
cy.location('hash').should('eq', '#section1')
Custom Assertions
// Add custom assertion
chai.Assertion.addMethod('beValidEmail', function() {
const obj = this._obj
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
this.assert(
emailRegex.test(obj),
'expected #{this} to be a valid email',
'expected #{this} not to be a valid email',
true
)
})
// Usage
cy.get('input[type="email"]')
.invoke('val')
.should('beValidEmail')
Network Testing
Intercepting Requests
// Basic intercept
cy.intercept('GET', '/api/users').as('getUsers')
cy.visit('/users')
cy.wait('@getUsers')
// Intercept with response modification
cy.intercept('GET', '/api/users', { fixture: 'users.json' }).as('getUsers')
// Intercept with dynamic response
cy.intercept('GET', '/api/users', (req) => {
req.reply({
statusCode: 200,
body: {
users: [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' }
]
}
})
}).as('getUsers')
// Intercept POST requests
cy.intercept('POST', '/api/users', {
statusCode: 201,
body: { id: 3, name: 'New User' }
}).as('createUser')
Request Assertions
// Wait and assert request
cy.intercept('GET', '/api/users').as('getUsers')
cy.visit('/users')
cy.wait('@getUsers').then((interception) => {
expect(interception.response.statusCode).to.eq(200)
expect(interception.response.body).to.have.property('users')
})
// Assert request was made
cy.intercept('POST', '/api/users').as('createUser')
cy.get('[data-cy="create-user"]').click()
cy.wait('@createUser').its('request.body').should('deep.include', {
name: 'John Doe',
email: 'john@example.com'
})
// Assert request headers
cy.wait('@getUsers').its('request.headers').should('have.property', 'authorization')
Network Conditions
// Simulate slow network
cy.intercept('GET', '/api/users', (req) => {
req.reply((res) => {
res.delay(2000) // 2 second delay
res.send({ fixture: 'users.json' })
})
}).as('getUsers')
// Simulate network failure
cy.intercept('GET', '/api/users', { forceNetworkError: true }).as('getUsersError')
// Simulate server error
cy.intercept('GET', '/api/users', {
statusCode: 500,
body: { error: 'Internal Server Error' }
}).as('getUsersError')
Multiple Intercepts
// Multiple endpoints
cy.intercept('GET', '/api/users').as('getUsers')
cy.intercept('GET', '/api/posts').as('getPosts')
cy.intercept('GET', '/api/comments').as('getComments')
cy.visit('/dashboard')
// Wait for all requests
cy.wait(['@getUsers', '@getPosts', '@getComments'])
// Or wait individually
cy.wait('@getUsers')
cy.wait('@getPosts')
cy.wait('@getComments')
File Operations
File Upload
// Upload single file
cy.get('input[type="file"]').selectFile('cypress/fixtures/example.pdf')
// Upload multiple files
cy.get('input[type="file"]').selectFile([
'cypress/fixtures/file1.jpg',
'cypress/fixtures/file2.jpg'
])
// Upload with drag and drop
cy.get('.dropzone').selectFile('cypress/fixtures/example.pdf', {
action: 'drag-drop'
})
// Upload from fixtures
cy.fixture('example.pdf', 'base64').then(fileContent => {
cy.get('input[type="file"]').selectFile({
contents: Cypress.Buffer.from(fileContent, 'base64'),
fileName: 'example.pdf',
mimeType: 'application/pdf'
})
})
File Download
// Download file and verify
cy.get('[data-cy="download-btn"]').click()
cy.readFile('cypress/downloads/report.pdf').should('exist')
// Verify downloaded file content
cy.get('[data-cy="download-csv"]').click()
cy.readFile('cypress/downloads/data.csv')
.should('contain', 'Name,Email,Age')
.and('contain', 'John Doe,john@example.com,30')
// Download with custom filename
cy.get('[data-cy="download-btn"]').click()
cy.task('downloadFile', {
url: 'https://example.com/file.pdf',
directory: 'cypress/downloads',
filename: 'custom-name.pdf'
})
Working with Fixtures
// Load fixture data
cy.fixture('users.json').then((users) => {
cy.intercept('GET', '/api/users', { body: users }).as('getUsers')
})
// Use fixture in test
cy.fixture('user-data.json').as('userData')
cy.get('@userData').then((data) => {
cy.get('[data-cy="name"]').type(data.name)
cy.get('[data-cy="email"]').type(data.email)
})
// Fixture with alias
cy.fixture('config.json').as('config')
cy.get('@config').its('apiUrl').should('eq', 'https://api.example.com')
Reading/Writing Files
// Read file
cy.readFile('cypress/fixtures/data.json').then((data) => {
expect(data).to.have.property('users')
})
// Write file
cy.writeFile('cypress/fixtures/output.json', {
timestamp: Date.now(),
testResults: 'passed'
})
// Append to file
cy.writeFile('cypress/fixtures/log.txt', 'New log entry\n', { flag: 'a+' })
Custom Commands
Basic Custom Commands
// cypress/support/commands.js
// Simple command
Cypress.Commands.add('login', (email, password) => {
cy.visit('/login')
cy.get('[data-cy="email"]').type(email)
cy.get('[data-cy="password"]').type(password)
cy.get('[data-cy="submit"]').click()
})
// Command with options
Cypress.Commands.add('login', (email, password, options = {}) => {
const { rememberMe = false } = options
cy.visit('/login')
cy.get('[data-cy="email"]').type(email)
cy.get('[data-cy="password"]').type(password)
if (rememberMe) {
cy.get('[data-cy="remember-me"]').check()
}
cy.get('[data-cy="submit"]').click()
})
// Usage
cy.login('user@example.com', 'password123')
cy.login('user@example.com', 'password123', { rememberMe: true })
Advanced Custom Commands
// Command that returns value
Cypress.Commands.add('getLocalStorage', (key) => {
return cy.window().then((win) => {
return win.localStorage.getItem(key)
})
})
// Command with chaining
Cypress.Commands.add('dataCy', (value) => {
return cy.get(`[data-cy=${value}]`)
})
// Usage with chaining
cy.dataCy('submit-btn').click()
cy.dataCy('username').type('john@example.com')
// Command with retry logic
Cypress.Commands.add('waitForElement', (selector, timeout = 10000) => {
cy.get(selector, { timeout }).should('be.visible')
})
// API command
Cypress.Commands.add('apiLogin', (email, password) => {
cy.request({
method: 'POST',
url: '/api/auth/login',
body: { email, password }
}).then((response) => {
window.localStorage.setItem('authToken', response.body.token)
})
})
Overwriting Commands
// Overwrite existing command
Cypress.Commands.overwrite('visit', (originalFn, url, options) => {
const domain = Cypress.config('baseUrl')
if (domain) {
const combinedUrl = `${domain}${url}`
return originalFn(combinedUrl, options)
}
return originalFn(url, options)
})
// Overwrite type command to clear first
Cypress.Commands.overwrite('type', (originalFn, element, text, options) => {
if (options && options.clearFirst !== false) {
element.clear()
}
return originalFn(element, text, options)
})
TypeScript Support
// cypress/support/commands.ts
declare global {
namespace Cypress {
interface Chainable {
login(email: string, password: string, options?: { rememberMe?: boolean }): Chainable<void>
dataCy(value: string): Chainable<JQuery<HTMLElement>>
apiLogin(email: string, password: string): Chainable<void>
}
}
}
Cypress.Commands.add('login', (email: string, password: string, options = {}) => {
// Implementation
})
Configuration
Cypress Configuration
// cypress.config.js
const { defineConfig } = require('cypress')
module.exports = defineConfig({
e2e: {
// Base URL for cy.visit() and cy.request()
baseUrl: 'http://localhost:3000',
// Viewport settings
viewportWidth: 1280,
viewportHeight: 720,
// Timeouts
defaultCommandTimeout: 10000,
requestTimeout: 10000,
responseTimeout: 30000,
pageLoadTimeout: 30000,
// Test files
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
excludeSpecPattern: ['**/examples/*', '**/temp/*'],
// Support file
supportFile: 'cypress/support/e2e.js',
// Fixtures
fixturesFolder: 'cypress/fixtures',
// Screenshots and videos
screenshotsFolder: 'cypress/screenshots',
videosFolder: 'cypress/videos',
video: true,
screenshotOnRunFailure: true,
// Browser settings
chromeWebSecurity: false,
// Setup function
setupNodeEvents(on, config) {
// implement node event listeners here
},
},
// Component testing
component: {
devServer: {
framework: 'react',
bundler: 'webpack',
},
},
})
Environment Variables
// cypress.config.js
module.exports = defineConfig({
e2e: {
env: {
apiUrl: 'https://api.example.com',
username: 'testuser',
password: 'testpass'
}
}
})
// cypress.env.json
{
"apiUrl": "https://api.example.com",
"username": "testuser",
"password": "testpass"
}
// Usage in tests
cy.visit(Cypress.env('apiUrl'))
const username = Cypress.env('username')
Multiple Environments
// cypress.config.js
module.exports = defineConfig({
e2e: {
setupNodeEvents(on, config) {
const environment = config.env.environment || 'development'
const environments = {
development: {
baseUrl: 'http://localhost:3000',
apiUrl: 'http://localhost:3001'
},
staging: {
baseUrl: 'https://staging.example.com',
apiUrl: 'https://api-staging.example.com'
},
production: {
baseUrl: 'https://example.com',
apiUrl: 'https://api.example.com'
}
}
config.baseUrl = environments[environment].baseUrl
config.env.apiUrl = environments[environment].apiUrl
return config
}
}
})
// Run with specific environment
// npx cypress run --env environment=staging
Browser Configuration
// cypress.config.js
module.exports = defineConfig({
e2e: {
// Browser launch options
setupNodeEvents(on, config) {
on('before:browser:launch', (browser = {}, launchOptions) => {
if (browser.name === 'chrome') {
launchOptions.args.push('--disable-dev-shm-usage')
launchOptions.args.push('--no-sandbox')
launchOptions.args.push('--disable-gpu')
}
if (browser.name === 'firefox') {
launchOptions.preferences['media.navigator.permission.disabled'] = true
}
return launchOptions
})
}
}
})
Page Objects
Basic Page Object
// cypress/support/pages/LoginPage.js
class LoginPage {
visit() {
cy.visit('/login')
return this
}
fillEmail(email) {
cy.get('[data-cy="email"]').type(email)
return this
}
fillPassword(password) {
cy.get('[data-cy="password"]').type(password)
return this
}
submit() {
cy.get('[data-cy="submit"]').click()
return this
}
login(email, password) {
this.fillEmail(email)
this.fillPassword(password)
this.submit()
return this
}
// Getters for elements
get emailInput() {
return cy.get('[data-cy="email"]')
}
get passwordInput() {
return cy.get('[data-cy="password"]')
}
get submitButton() {
return cy.get('[data-cy="submit"]')
}
get errorMessage() {
return cy.get('[data-cy="error-message"]')
}
}
export default LoginPage
Using Page Objects
// cypress/e2e/login.cy.js
import LoginPage from '../support/pages/LoginPage'
describe('Login Tests', () => {
const loginPage = new LoginPage()
beforeEach(() => {
loginPage.visit()
})
it('should login successfully', () => {
loginPage
.login('user@example.com', 'password123')
cy.url().should('include', '/dashboard')
})
it('should show error for invalid credentials', () => {
loginPage
.login('invalid@email.com', 'wrongpassword')
loginPage.errorMessage
.should('be.visible')
.and('contain', 'Invalid credentials')
})
})
```### 고급 페이지 객체
```javascript
// cypress/support/pages/DashboardPage.js
class DashboardPage {
constructor() {
this.selectors = {
userMenu: '[data-cy="user-menu"]',
userProfile: '[data-cy="user-profile"]',
notifications: '[data-cy="notifications"]',
searchInput: '[data-cy="search"]',
sidebar: '[data-cy="sidebar"]',
mainContent: '[data-cy="main-content"]'
}
}
visit() {
cy.visit('/dashboard')
this.waitForLoad()
return this
}
waitForLoad() {
cy.get(this.selectors.mainContent).should('be.visible')
cy.get('.loading-spinner').should('not.exist')
return this
}
openUserMenu() {
cy.get(this.selectors.userMenu).click()
return this
}
search(query) {
cy.get(this.selectors.searchInput)
.clear()
.type(`${query}{enter}`)
return this
}
// Navigation methods
navigateToProfile() {
this.openUserMenu()
cy.get(this.selectors.userProfile).click()
return this
}
// Assertion methods
shouldShowWelcomeMessage(username) {
cy.contains(`Welcome, ${username}`).should('be.visible')
return this
}
shouldHaveNotificationCount(count) {
cy.get(this.selectors.notifications)
.find('.badge')
.should('contain', count)
return this
}
}
export default DashboardPage
```### API 통합 페이지 객체
```javascript
// cypress/support/pages/UsersPage.js
class UsersPage {
visit() {
cy.intercept('GET', '/api/users').as('getUsers')
cy.visit('/users')
cy.wait('@getUsers')
return this
}
createUser(userData) {
cy.intercept('POST', '/api/users').as('createUser')
cy.get('[data-cy="create-user-btn"]').click()
cy.get('[data-cy="name"]').type(userData.name)
cy.get('[data-cy="email"]').type(userData.email)
cy.get('[data-cy="save"]').click()
cy.wait('@createUser')
return this
}
deleteUser(userId) {
cy.intercept('DELETE', `/api/users/${userId}`).as('deleteUser')
cy.get(`[data-cy="user-${userId}"]`)
.find('[data-cy="delete-btn"]')
.click()
cy.get('[data-cy="confirm-delete"]').click()
cy.wait('@deleteUser')
return this
}
shouldShowUser(userData) {
cy.get('[data-cy="users-table"]')
.should('contain', userData.name)
.and('contain', userData.email)
return this
}
}
export default UsersPage
```## API 테스팅
```javascript
describe('API Tests', () => {
it('should get users', () => {
cy.request('GET', '/api/users')
.then((response) => {
expect(response.status).to.eq(200)
expect(response.body).to.have.property('users')
expect(response.body.users).to.be.an('array')
})
})
it('should create user', () => {
const newUser = {
name: 'John Doe',
email: 'john@example.com'
}
cy.request('POST', '/api/users', newUser)
.then((response) => {
expect(response.status).to.eq(201)
expect(response.body).to.have.property('id')
expect(response.body.name).to.eq(newUser.name)
expect(response.body.email).to.eq(newUser.email)
})
})
})
```### 기본 API 테스트
```javascript
describe('Authenticated API Tests', () => {
let authToken
before(() => {
// Login and get token
cy.request({
method: 'POST',
url: '/api/auth/login',
body: {
email: 'admin@example.com',
password: 'password123'
}
}).then((response) => {
authToken = response.body.token
})
})
it('should get protected resource', () => {
cy.request({
method: 'GET',
url: '/api/admin/users',
headers: {
'Authorization': `Bearer ${authToken}`
}
}).then((response) => {
expect(response.status).to.eq(200)
expect(response.body).to.have.property('users')
})
})
it('should fail without token', () => {
cy.request({
method: 'GET',
url: '/api/admin/users',
failOnStatusCode: false
}).then((response) => {
expect(response.status).to.eq(401)
})
})
})
```### 인증이 있는 API 테스트
```javascript
// cypress/support/api-helpers.js
Cypress.Commands.add('apiLogin', (email, password) => {
return cy.request({
method: 'POST',
url: '/api/auth/login',
body: { email, password }
}).then((response) => {
window.localStorage.setItem('authToken', response.body.token)
return response.body.token
})
})
Cypress.Commands.add('apiRequest', (method, url, body = null) => {
const token = window.localStorage.getItem('authToken')
return cy.request({
method,
url,
body,
headers: token ? { 'Authorization': `Bearer ${token}` } : {}
})
})
// Usage
cy.apiLogin('admin@example.com', 'password123')
cy.apiRequest('GET', '/api/users')
cy.apiRequest('POST', '/api/users', { name: 'John', email: 'john@example.com' })
```### API 테스트 헬퍼
```javascript
// Install ajv for JSON schema validation
// npm install --save-dev ajv
// cypress/support/schema-validation.js
import Ajv from 'ajv'
const ajv = new Ajv()
Cypress.Commands.add('validateSchema', (data, schema) => {
const validate = ajv.compile(schema)
const valid = validate(data)
if (!valid) {
throw new Error(`Schema validation failed: ${JSON.stringify(validate.errors)}`)
}
})
// Usage
const userSchema = {
type: 'object',
properties: {
id: { type: 'number' },
name: { type: 'string' },
email: { type: 'string', format: 'email' }
},
required: ['id', 'name', 'email']
}
cy.request('GET', '/api/users/1')
.then((response) => {
cy.validateSchema(response.body, userSchema)
})
```### 스키마 검증
```javascript
describe('Visual Tests', () => {
it('should match homepage screenshot', () => {
cy.visit('/')
cy.screenshot('homepage')
})
it('should match element screenshot', () => {
cy.visit('/dashboard')
cy.get('[data-cy="user-profile"]').screenshot('user-profile')
})
it('should match full page screenshot', () => {
cy.visit('/products')
cy.screenshot('products-page', { capture: 'fullPage' })
})
})
```## 시각적 테스팅
```bash
# Install Percy
npm install --save-dev @percy/cypress
// cypress/support/e2e.js
import '@percy/cypress'
// Usage in tests
describe('Visual Regression Tests', () => {
it('should match visual snapshot', () => {
cy.visit('/')
cy.percySnapshot('Homepage')
})
it('should match responsive snapshots', () => {
cy.visit('/dashboard')
cy.percySnapshot('Dashboard - Desktop', {
widths: [1280]
})
cy.percySnapshot('Dashboard - Mobile', {
widths: [375]
})
})
})
```### 스크린샷 테스팅
```javascript
// cypress/support/commands.js
Cypress.Commands.add('compareSnapshot', (name, options = {}) => {
const { threshold = 0.1, clip } = options
cy.screenshot(name, { clip })
// Custom comparison logic would go here
// This is a simplified example
cy.task('compareImages', {
actualImage: `cypress/screenshots/${name}.png`,
expectedImage: `cypress/snapshots/${name}.png`,
threshold
})
})
// Usage
cy.visit('/login')
cy.compareSnapshot('login-page', { threshold: 0.05 })
```### Percy를 이용한 시각적 회귀 테스트
```javascript
describe('Responsive Tests', () => {
const viewports = [
{ width: 320, height: 568 }, // iPhone SE
{ width: 375, height: 667 }, // iPhone 8
{ width: 768, height: 1024 }, // iPad
{ width: 1024, height: 768 }, // iPad Landscape
{ width: 1280, height: 720 }, // Desktop
]
viewports.forEach((viewport) => {
it(`should display correctly at ${viewport.width}x${viewport.height}`, () => {
cy.viewport(viewport.width, viewport.height)
cy.visit('/')
cy.screenshot(`homepage-${viewport.width}x${viewport.height}`)
// Test responsive behavior
if (viewport.width < 768) {
cy.get('[data-cy="mobile-menu"]').should('be.visible')
cy.get('[data-cy="desktop-menu"]').should('not.be.visible')
} else {
cy.get('[data-cy="mobile-menu"]').should('not.be.visible')
cy.get('[data-cy="desktop-menu"]').should('be.visible')
}
})
})
})
```### 커스텀 시각적 명령어
```yaml
# .github/workflows/cypress.yml
name: Cypress Tests
on: [push, pull_request]
jobs:
cypress-run:
runs-on: ubuntu-latest
strategy:
matrix:
browser: [chrome, firefox, edge]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Start application
run: npm start &
- name: Wait for application
run: npx wait-on http://localhost:3000
- name: Run Cypress tests
uses: cypress-io/github-action@v5
with:
browser: ${{ matrix.browser }}
record: true
parallel: true
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload screenshots
uses: actions/upload-artifact@v3
if: failure()
with:
name: cypress-screenshots-${{ matrix.browser }}
path: cypress/screenshots
- name: Upload videos
uses: actions/upload-artifact@v3
if: always()
with:
name: cypress-videos-${{ matrix.browser }}
path: cypress/videos
```### 반응형 테스팅
```dockerfile
# Dockerfile.cypress
FROM cypress/included:12.17.0
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# Run tests
CMD ["npx", "cypress", "run"]
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=test
cypress:
build:
context: .
dockerfile: Dockerfile.cypress
depends_on:
- app
environment:
- CYPRESS_baseUrl=http://app:3000
volumes:
- ./cypress/videos:/app/cypress/videos
- ./cypress/screenshots:/app/cypress/screenshots
```## CI/CD 통합
```javascript
// cypress.config.js
module.exports = defineConfig({
e2e: {
// Enable parallel testing
setupNodeEvents(on, config) {
// Split tests across multiple machines
if (config.env.SPLIT_TESTS) {
const totalMachines = config.env.TOTAL_MACHINES || 1
const currentMachine = config.env.CURRENT_MACHINE || 0
// Custom logic to split test files
config.specPattern = getSpecsForMachine(currentMachine, totalMachines)
}
return config
}
}
})
function getSpecsForMachine(current, total) {
// Implementation to split specs across machines
const allSpecs = [
'cypress/e2e/auth/*.cy.js',
'cypress/e2e/dashboard/*.cy.js',
'cypress/e2e/users/*.cy.js'
]
return allSpecs.filter((_, index) => index % total === current)
}
```### GitHub Actions
```bash
# Install mochawesome reporter
npm install --save-dev mochawesome mochawesome-merge mochawesome-report-generator
// cypress.config.js
module.exports = defineConfig({
e2e: {
reporter: 'mochawesome',
reporterOptions: {
reportDir: 'cypress/reports',
overwrite: false,
html: false,
json: true
}
}
})
{
"scripts": {
"test:report": "cypress run && npm run merge:reports && npm run generate:report",
"merge:reports": "mochawesome-merge cypress/reports/*.json > cypress/reports/merged-report.json",
"generate:report": "marge cypress/reports/merged-report.json --reportDir cypress/reports --inline"
}
}
```### Docker 통합
```javascript
// Good: Descriptive test names
describe('User Authentication', () => {
it('should allow user to login with valid credentials', () => {
// Test implementation
})
it('should display error message for invalid credentials', () => {
// Test implementation
})
it('should redirect to dashboard after successful login', () => {
// Test implementation
})
})
// Good: Group related tests
describe('Shopping Cart', () => {
describe('Adding Items', () => {
it('should add item to cart')
it('should update cart count')
it('should show item in cart sidebar')
})
describe('Removing Items', () => {
it('should remove item from cart')
it('should update cart total')
it('should show empty cart message when no items')
})
})
```### 병렬 테스팅
```javascript
// ❌ Bad: Fragile selectors
cy.get('.btn.btn-primary.submit-button')
cy.get('div > form > div:nth-child(2) > input')
cy.get('button:contains("Submit")')
// ✅ Good: Stable selectors
cy.get('[data-cy="submit-button"]')
cy.get('[data-testid="email-input"]')
cy.get('[aria-label="Submit form"]')
// ✅ Good: Custom commands for common patterns
Cypress.Commands.add('getByTestId', (testId) => {
return cy.get(`[data-testid="${testId}"]`)
})
cy.getByTestId('email-input').type('user@example.com')
```### 테스트 보고
```javascript
// ❌ Bad: Arbitrary waits
cy.wait(3000)
cy.get('button').click()
// ✅ Good: Wait for specific conditions
cy.get('[data-cy="loading"]').should('not.exist')
cy.get('[data-cy="submit-button"]').should('be.enabled').click()
// ✅ Good: Wait for network requests
cy.intercept('POST', '/api/users').as('createUser')
cy.get('[data-cy="submit"]').click()
cy.wait('@createUser')
```## 모범 사례
```javascript
// ✅ Good: Use fixtures for test data
cy.fixture('users.json').then((users) => {
const testUser = users.validUser
cy.login(testUser.email, testUser.password)
})
// ✅ Good: Generate dynamic test data
const generateUser = () => ({
name: `Test User ${Date.now()}`,
email: `test${Date.now()}@example.com`,
password: 'password123'
})
// ✅ Good: Clean up test data
afterEach(() => {
cy.task('cleanupTestData')
})
```### 테스트 구조화
```javascript
// ✅ Good: Handle expected errors
cy.get('[data-cy="submit"]').click()
cy.get('[data-cy="error-message"]')
.should('be.visible')
.and('contain', 'Email is required')
// ✅ Good: Retry flaky operations
Cypress.Commands.add('retryableClick', (selector, maxRetries = 3) => {
let attempts = 0
const clickWithRetry = () => {
attempts++
return cy.get(selector).click().then(() => {
// Verify click was successful
return cy.get(selector).should('have.class', 'clicked')
}).catch((error) => {
if (attempts < maxRetries) {
cy.wait(1000)
return clickWithRetry()
}
throw error
})
}
return clickWithRetry()
})
```### 셀렉터 모범 사례
```javascript
// Pause test execution
cy.pause()
// Debug specific element
cy.get('[data-cy="button"]').debug()
// Log values
cy.get('[data-cy="input"]').then(($el) => {
console.log('Element value:', $el.val())
})
// Take screenshot at specific point
cy.screenshot('debug-point-1')
// Log network requests
cy.intercept('**', (req) => {
console.log('Request:', req.method, req.url)
})
```### 브라우저 개발자 도구
```javascript
// Open DevTools programmatically
cy.window().then((win) => {
win.debugger
})
// Inspect element in DevTools
cy.get('[data-cy="element"]').then(($el) => {
debugger // Will pause in DevTools
console.log($el[0]) // Inspect element
})
// Access application state
cy.window().its('store').then((store) => {
console.log('Redux state:', store.getState())
})
```### 사용자 정의 디버그 명령
```javascript
// cypress/support/commands.js
Cypress.Commands.add('logElement', (selector) => {
cy.get(selector).then(($el) => {
console.log('Element:', $el[0])
console.log('Text:', $el.text())
console.log('Value:', $el.val())
console.log('Classes:', $el.attr('class'))
})
})
Cypress.Commands.add('debugState', () => {
cy.window().then((win) => {
if (win.store) {
console.log('Redux State:', win.store.getState())
}
if (win.localStorage) {
console.log('LocalStorage:', win.localStorage)
}
if (win.sessionStorage) {
console.log('SessionStorage:', win.sessionStorage)
}
})
})
// Usage
cy.logElement('[data-cy="user-profile"]')
cy.debugState()
```### 테스트 디버깅 전략
```javascript
// Strategy 1: Isolate failing test
it.only('should debug this specific test', () => {
// Test implementation
})
// Strategy 2: Add detailed logging
it('should complete user flow', () => {
cy.log('Step 1: Visit login page')
cy.visit('/login')
cy.log('Step 2: Fill credentials')
cy.get('[data-cy="email"]').type('user@example.com')
cy.get('[data-cy="password"]').type('password123')
cy.log('Step 3: Submit form')
cy.get('[data-cy="submit"]').click()
cy.log('Step 4: Verify redirect')
cy.url().should('include', '/dashboard')
})
// Strategy 3: Break down complex tests
it('should login user', () => {
cy.login('user@example.com', 'password123')
})
it('should navigate to profile', () => {
cy.login('user@example.com', 'password123')
cy.get('[data-cy="profile-link"]').click()
cy.url().should('include', '/profile')
})
```## 성능
```javascript
// Optimize test setup
describe('Performance Optimized Tests', () => {
// Use beforeEach for common setup
beforeEach(() => {
cy.login('user@example.com', 'password123')
})
// Avoid unnecessary visits
it('should test multiple features on same page', () => {
cy.visit('/dashboard')
// Test multiple things on the same page
cy.get('[data-cy="user-name"]').should('contain', 'John Doe')
cy.get('[data-cy="notification-count"]').should('contain', '3')
cy.get('[data-cy="recent-activity"]').should('be.visible')
})
})
// Use API for setup when possible
Cypress.Commands.add('seedData', () => {
return cy.request('POST', '/api/test/seed', {
users: 10,
posts: 50,
comments: 100
})
})
// Usage
beforeEach(() => {
cy.seedData()
})
```### 테스트 성능
```bash
# Run tests in parallel (requires Cypress Dashboard)
npx cypress run --record --parallel
# Split tests manually
npx cypress run --spec "cypress/e2e/auth/**/*"
npx cypress run --spec "cypress/e2e/dashboard/**/*"
npx cypress run --spec "cypress/e2e/users/**/*"
```### 병렬 실행
```javascript
// cypress.config.js
module.exports = defineConfig({
e2e: {
// Optimize memory usage
numTestsKeptInMemory: 5,
// Disable video for faster execution
video: false,
// Reduce screenshot quality
screenshotOnRunFailure: true,
setupNodeEvents(on, config) {
// Clear browser cache between tests
on('before:browser:launch', (browser, launchOptions) => {
if (browser.name === 'chrome') {
launchOptions.args.push('--disable-dev-shm-usage')
launchOptions.args.push('--memory-pressure-off')
}
return launchOptions
})
}
}
})
```### 메모리 최적화
```javascript
// Issue: Element not found
// Solution: Wait for element or check selector
cy.get('[data-cy="button"]', { timeout: 10000 }).should('exist')
// Issue: Element not clickable
// Solution: Ensure element is visible and enabled
cy.get('[data-cy="button"]')
.should('be.visible')
.and('not.be.disabled')
.click()
// Issue: Flaky tests
// Solution: Add proper waits and assertions
cy.intercept('GET', '/api/data').as('getData')
cy.visit('/page')
cy.wait('@getData')
cy.get('[data-cy="content"]').should('be.visible')
// Issue: Cross-origin errors
// Solution: Disable web security (not recommended for production)
// cypress.config.js
{
chromeWebSecurity: false
}
```## 문제 해결
```javascript
// cypress.config.js
module.exports = defineConfig({
e2e: {
// Enable debug mode
setupNodeEvents(on, config) {
// Log all events
on('task', {
log(message) {
console.log(message)
return null
}
})
// Debug failed tests
on('after:spec', (spec, results) => {
if (results && results.stats.failures) {
console.log('Failed spec:', spec.relative)
console.log('Failures:', results.stats.failures)
}
})
}
}
})
// Usage in tests
cy.task('log', 'Debug message here')
```### 일반적인 문제
```javascript
// Retry failed commands
Cypress.Commands.add('retryCommand', (command, maxRetries = 3) => {
let attempts = 0
const executeCommand = () => {
attempts++
return command().catch((error) => {
if (attempts < maxRetries) {
cy.wait(1000)
return executeCommand()
}
throw error
})
}
return executeCommand()
})
// Usage
cy.retryCommand(() => cy.get('[data-cy="flaky-element"]').click())
```### 디버그 구성