이미 종단 간(end-to-end) 테스트를 위해 Cypress를 실행하고 있습니다. 이제 UI를 클릭하는 과정 없이 API에 직접 요청을 보내고, 상태 코드를 확인하며, JSON 본문을 단언하고 싶을 것입니다. Cypress는 이를 수행할 수 있습니다. cy.request() 명령어는 브라우저 외부의 Node에서 실제 HTTP 요청을 보내므로, 엔드포인트를 단독으로 테스트하거나 UI 테스트가 실행되기 전에 상태를 설정할 수 있습니다.
이 가이드에서는 Cypress로 API를 테스트하는 방법을 보여줍니다: cy.request()의 기본 사항, 응답 단언하기, 인증 토큰 재사용을 위한 요청 체이닝, 그리고 cy.intercept()를 사용하여 브라우저 트래픽 스텁하기. 또한 API 테스트에 Cypress가 적합한 경우와 API 네이티브 도구가 더 적합한 경우를 알아볼 것입니다.
Cypress로 API를 테스트하는 이유
대부분의 Cypress 스위트는 브라우저를 구동합니다. 하지만 UI에서 확인하는 많은 것들은 그 아래에 있는 API와 관련이 있습니다: /login이 토큰을 반환하는지, /users가 올바른 형태를 반환하는지, POST 요청이 예상하는 레코드를 생성하는지 등입니다.
이러한 엔드포인트를 직접 테스트하면 두 가지 이점이 있습니다. 첫째, 렌더링이나 요소를 기다리는 과정이 없기 때문에 API 검사가 UI 흐름보다 빠르게 실행됩니다. 둘째, cy.request()를 사용하여 테스트 데이터를 설정할 수 있습니다. 사용자 생성을 위해 가입 양식을 클릭하는 대신, 단 한 번의 POST 요청을 보내고 실제로 관심 있는 단언으로 넘어갈 수 있습니다.
만약 Cypress가 이미 스택에 있다면, API 테스트를 추가하는 것은 새로운 도구를 설치할 필요가 없다는 의미입니다. 동일한 스펙 파일에, 동일한 expect 단언을 사용하여, 동일한 CI 실행에서 작성할 수 있습니다.
cy.request() 기본 사항
cy.request()는 HTTP 요청을 생성하고 응답을 반환합니다. 이 명령은 브라우저가 아닌 Cypress Node 프로세스에서 실행되므로, CORS 또는 동일 출처(same-origin) 규칙의 적용을 받지 않습니다.
이 명령어는 여러 형태를 지원합니다:
cy.request(url)
cy.request(url, body)
cy.request(method, url)
cy.request(method, url, body)
cy.request(options)
가장 간단한 호출은 URL을 전달합니다. 메서드가 없으면 Cypress는 기본적으로 GET을 사용합니다:
cy.request('https://jsonplaceholder.typicode.com/users')
기본적인 GET 요청을 넘어서는 모든 요청에는 옵션 객체를 전달합니다. 이를 통해 메서드, 본문 및 헤더를 완벽하게 제어할 수 있습니다:
cy.request({
method: 'POST',
url: 'https://jsonplaceholder.typicode.com/posts',
headers: {
'Content-Type': 'application/json'
},
body: {
title: 'API test',
body: 'created from Cypress',
userId: 1
}
})
해당 객체에서 유용한 옵션:
method: HTTP 동사(GET,POST,PUT,PATCH,DELETE및 기타). 기본값은GET입니다.url: 엔드포인트. 필수입니다.body: 요청 페이로드. Cypress는 객체를 JSON으로 직렬화합니다.headers: 요청 헤더의 객체입니다.auth: HTTP 기본 인증을 위한 자격 증명입니다.qs: 객체 형태의 쿼리 스트링 매개변수입니다.failOnStatusCode: 4xx 또는 5xx 응답에 대해 테스트 실패 대신 단언하고 싶을 때false로 설정합니다.
마지막 옵션은 부정 테스트(negative tests)에 중요합니다. 기본적으로 cy.request()는 2xx 또는 3xx가 아닌 모든 상태에서 테스트를 실패시킵니다. 잘못된 요청이 400을 반환하는지 확인하려면, 이 옵션을 명시적으로 설정해야 합니다:
cy.request({
method: 'GET',
url: 'https://jsonplaceholder.typicode.com/users/99999',
failOnStatusCode: false
}).then((response) => {
expect(response.status).to.eq(404)
})
상태 및 본문 단언하기
cy.request()는 응답 객체를 반환합니다. 이를 읽으려면 .then()을 체이닝합니다. 응답에는 주로 사용할 네 가지 속성이 있습니다:
status: HTTP 상태 코드입니다.body: 응답 페이로드. Cypress는Content-Type이json으로 끝날 때 이를 JavaScript 객체로 파싱합니다.headers: 응답 헤더입니다.duration: 요청이 걸린 시간(밀리초)입니다.
다음은 상태, 본문 형태 및 특정 필드를 확인하는 전체 테스트입니다:
describe('Users API', () => {
it('returns a list of users', () => {
cy.request('https://jsonplaceholder.typicode.com/users').then((response) => {
expect(response.status).to.eq(200)
expect(response.body).to.be.an('array')
expect(response.body).to.have.length(10)
expect(response.body[0]).to.have.property('email')
})
})
})
response.body는 이미 파싱된 객체이므로, 표준 Chai를 사용하여 단언할 수 있습니다. 유형, 길이, 중첩된 속성 또는 정확한 값을 확인할 수 있습니다. 이는 UI 테스트에 사용하는 것과 동일한 단언 구문이므로, 새로 배울 것이 없습니다.
응답 헤더에 대해서도 단언할 수 있으며, 이는 콘텐츠 유형이나 캐싱을 확인할 때 유용합니다:
cy.request('https://jsonplaceholder.typicode.com/users').then((response) => {
expect(response.headers['content-type']).to.include('application/json')
})
이러한 검사를 구성하는 방법에 대한 더 자세한 내용은 API 단언 가이드와 더 넓은 범위의 API 테스트 모범 사례를 참조하십시오.
요청 체이닝 및 인증 토큰 재사용
실제 API는 인증이 필요합니다. 일반적인 패턴은 한 번 로그인하여 토큰을 얻은 다음, 이어지는 모든 요청에 해당 토큰을 보내는 것입니다. cy.request()는 이 목적을 위해 깔끔하게 체이닝됩니다.
로그인 요청을 보내고, 응답 본문에서 토큰을 읽은 다음, 다음 호출에서 사용합니다:
describe('Authenticated API flow', () => {
it('logs in and fetches a protected resource', () => {
cy.request({
method: 'POST',
url: 'https://api.example.com/login',
body: {
email: 'user@example.com',
password: 'secret'
}
}).then((loginResponse) => {
expect(loginResponse.status).to.eq(200)
const token = loginResponse.body.token
cy.request({
method: 'GET',
url: 'https://api.example.com/profile',
headers: {
Authorization: `Bearer ${token}`
}
}).then((profileResponse) => {
expect(profileResponse.status).to.eq(200)
expect(profileResponse.body).to.have.property('email', 'user@example.com')
})
})
})
})
```여러 테스트에서 동일한 토큰이 필요한 경우, 로그인을 beforeEach 블록으로 이동하고 Cypress.env() 또는 래핑된 별칭을 사용하여 토큰을 저장함으로써 각 테스트가 이를 사용할 수 있도록 합니다. 이는 모든 스펙에서 반복하는 대신 인증 설정을 한 곳에 유지합니다.
이 '로그인 후 사용' 패턴은 한 호출이 다음 호출에 데이터를 제공하는 모든 API 통합 테스트 흐름에서 작성하는 것과 동일한 형태입니다.
스텁을 위한 cy.intercept()
cy.request()는 실제 API에 요청을 보냅니다. 때로는 그 반대의 경우가 필요할 수 있습니다: 프론트엔드 앱이 보내는 요청을 가로채고 가짜 응답을 반환하는 것입니다. 이것이 바로 cy.intercept()입니다.
여기에는 중요한 차이점이 있습니다. cy.intercept()는 브라우저에서 프론트엔드 애플리케이션이 만드는 요청만 가로챕니다. cy.request()는 브라우저를 완전히 우회하기 때문에 cy.request()를 가로채지 않습니다. cy.intercept()는 API 자체를 테스트할 때가 아니라, UI가 주어진 API 응답에 어떻게 반응하는지 테스트할 때 사용하십시오.
요청을 감시하거나, 정적 응답으로 스텁하거나, 요청을 기다릴 수 있습니다. 스텁하려면 응답 객체를 전달합니다:
cy.intercept('GET', '/api/users', {
statusCode: 200,
body: [{ id: 1, name: 'Ada' }]
})
이제 /api/users에 대한 모든 브라우저 요청은 고정된 페이로드를 반환합니다. 이를 통해 빈 목록, 500 오류 또는 느린 응답과 같이 실제 백엔드에서는 트리거하기 어려운 엣지 케이스를 테스트할 수 있습니다.
요청이 발생했음을 단언하려면 .as()로 별칭을 지정하고 기다립니다:
it('shows an error banner when the API fails', () => {
cy.intercept('GET', '/api/users', {
statusCode: 500,
body: { message: 'Server error' }
}).as('getUsers')
cy.visit('/dashboard')
cy.wait('@getUsers').its('response.statusCode').should('eq', 500)
cy.contains('Something went wrong').should('be.visible')
})
```cy.wait('@getUsers') 라인은 가로챈 요청이 해결될 때까지 테스트를 일시 중지한 다음, 단언할 요청과 응답을 제공합니다. 요청을 트리거하는 동작 이전에 인터셉트를 설정해야 합니다. 그렇지 않으면 아무것도 가로채지 못합니다. 응답을 가짜로 만들지 전체 서비스를 모의할지 저울질하고 있다면, API 목킹 및 API 스텁 vs API 목킹에 대한 게시물에서 장단점을 자세히 설명합니다.
API 테스트에 Cypress가 적합한 경우와 그렇지 않은 경우
Cypress는 UI 테스트 스위트 내부에 있는 API 검사에 강력하게 적합합니다. 이미 종단 간(e2e) 테스트를 작성하고 있고 데이터를 시드하거나, UI가 의존하는 엔드포인트를 확인하거나, 오류 처리를 테스트하기 위해 응답을 스텁해야 하는 경우, cy.request() 및 cy.intercept()는 모든 것을 한 곳에 유지합니다. 문맥 전환도, 두 번째 도구도 필요 없습니다.
또한 Cypress가 유일한 테스트 러너이고 다른 종속성을 추가하고 싶지 않을 때, 소수의 독립형 API 스모크 테스트에도 괜찮습니다.
어색해지는 지점은 크고 독립적인 API 테스트 스위트의 경우입니다. Cypress는 브라우저를 위해 구축되었으므로, API 테스트는 핵심 설계가 아닌 그 위에 계층화된 기능입니다. 스위트가 커지면서 몇 가지 약점이 드러납니다:
시각적인 요청 빌더 없음. 모든 요청은 코드입니다. 수백 개의 엔드포인트를 다룰 때 헤더, 본문, 쿼리 매개변수를 수동으로 구성하는 것은 느려집니다.CI 전용 API 파이프라인에는 약함. Cypress는 순수 API 실행에서도 브라우저 컨텍스트를 시작하며, 이는 불필요한 오버헤드입니다.공유되는 API 정의 없음. 요청 형태는 테스트 파일에 존재하며, 팀이 설계, 문서화, 테스트 전반에 걸쳐 재사용할 수 있는 스펙에 있지 않습니다.
이러한 경우 API 네이티브 도구가 더 적합합니다. Apidog는 API 자체를 중심으로 구축되었습니다: 엔드포인트를 한 번 설계하고, 시각적 요청 빌더와 시각적 단언을 사용하여 테스트 시나리오를 구축하며, 일반적인 경우에는 코드가 필요하지 않습니다. 요청, 환경 및 인증은 공유 작업 공간에 존재하므로, 팀은 테스트 파일에서 이를 다시 파생시킬 필요가 없습니다.
CI의 경우, Apidog CLI는 Node를 실행할 수 있는 모든 단계에서 저장된 테스트 시나리오를 헤드리스 모드로 실행합니다:
npm install -g apidog-cli
apidog run \
--access-token "$APIDOG_ACCESS_TOKEN" \
-t <scenarioOrSuiteId> \
-e <environmentId> \
-r cli,html,junit
-t 플래그는 저장된 시나리오 또는 스위트를 지정하고, -e는 환경을 선택하며, -r은 보고서 형식(cli, html, json, junit)을 선택합니다. JUnit 출력은 대부분의 CI 대시보드에 직접 연결됩니다. 데이터 파일에서 테스트를 구동하려면 -d 또는 --iteration-data를 추가하고, 결과를 작업 공간으로 푸시하려면 --upload-report를 추가합니다. CLI는 저장된 시나리오를 실행하며, 대화형 요청 발송기가 아닙니다.
이를 유용하게 생각하는 방법: 브라우저 테스트를 지원하는 API 검사에는 cy.request()를 사용하고, API 테스트가 주요 작업일 때는 API 네이티브 도구를 활용하십시오. 옵션을 비교하고 있다면, CI/CD에서 실행되는 API 테스트 자동화 도구 및 더 넓은 범위의 최고의 API 테스트 도구 30가지에 대한 저희의 요약이 이 분야를 다룹니다.
자주 묻는 질문
cy.request()는 브라우저에서 실행되나요?
아니요. cy.request()는 브라우저 외부의 Cypress Node 프로세스에서 실행됩니다. 그래서 CORS 또는 동일 출처 정책에 의해 차단되지 않으며, cy.intercept()가 이를 가로챌 수 없는 이유입니다. 브라우저가 만드는 요청이 필요한 경우, 앱을 통해 트리거하고 cy.intercept()로 가로채십시오.
테스트를 실패시키지 않고 400 또는 404 응답을 어떻게 테스트하나요?
기본적으로 cy.request()는 2xx 또는 3xx가 아닌 모든 상태에서 실패합니다. 옵션 객체에 failOnStatusCode: false를 설정한 다음, expect(response.status).to.eq(404)와 같이 직접 상태를 단언하십시오.
UI 테스트 전에 cy.request()를 사용하여 로그인할 수 있나요?
네, 가능하며 흔한 패턴입니다. cy.request()를 사용하여 로그인 엔드포인트에 POST 요청을 보내고, response.body에서 토큰을 읽은 다음, 이를 (Cypress.env(), localStorage 또는 쿠키에) 저장하여 UI 테스트가 이미 인증된 상태로 시작하도록 합니다. 이렇게 하면 로그인 양식을 건너뛰고 스위트 속도를 높일 수 있습니다.
cy.request()와 cy.intercept()의 차이점은 무엇인가요?
cy.request()는 실제 HTTP 요청을 보내고 실제 응답을 반환하므로, API를 테스트하는 데 사용합니다. cy.intercept()는 프론트엔드가 브라우저에서 만드는 요청을 감시하거나 가짜로 만드므로, UI가 받는 것을 제어하는 데 사용합니다. 하나는 네트워크에 직접 요청을 보내고, 다른 하나는 요청의 형태를 바꿉니다.
API 테스트를 위해 Cypress를 사용해야 할까요, 아니면 전용 도구를 사용해야 할까요?
이미 실행 중인 브라우저 테스트 스위트를 API 검사가 지원할 때 Cypress를 사용하십시오. 대규모 독립형 API 스위트, CI 전용 API 파이프라인, 또는 팀 전체가 사용하는 공유 API 정의의 경우, Apidog와 같은 API 네이티브 도구가 더 적합합니다. 결정하는 방법에 대해서는 API 테스트 모범 사례를 참조하십시오.
