이 포스팅 작성 시점 기준 cypress 버전은 v9.1.0 입니다.
Cypress가 기존 test 프레임워크와 다른 점
e2e test(end to end test)는 작은 단위의 유닛 테스트가 아닌 사용자 시나리오를 통째로 실행하여 정상적으로 기능하는지를 확인하는 기능 테스트입니다. cypress는 프론트엔드 e2e test framework이며 기존 프론트엔드 테스트 프레임워크들과는 다른 차별점을 가집니다.
먼저, cypress는 실제 돔에서 테스트합니다. jest를 이용한 react testing library와 같은 것도 실제 돔에서 테스트합니다만, cypress는 node위에 동작하는 서버를 띄우고 실제 브라우저까지 열어서 브라우저 위에서 테스트를 진행하고 그 위에서 time traveling, debugging, record 등을 할 수 있도록 기능을 지원합니다.
cypress에서 브레이크 포인트를 따로 찍을 수도 있지만, time traveling은 그 자체로도 강력한 디버깅 support입니다. 개발자는 기능 테스트의 실패를 봤을 때, 어떤 단계에서 어떻게 잘못된 것인지 GUI를 통해 세부적으로 들여다볼 수 있습니다.
또한 GUI는 크롬 브라우저를 제공하기에 크롬 개발자도구를 그대로 사용하여 디버깅을 할 수 있습니다. 타임 트라블링을 할 때에는 각 단계에 대해 클릭을 하면 개발자 도구 콘솔에 그 단계에 해당하는 데이터들을 출력해줍니다.
실제 유저 입장에서 테스트를 하기 때문에 아무리 유효한 실제 DOM이 있다고 해도 사용자가 행동할 수 없는 시나리오에 대해서는 테스트가 실패합니다. 예를 들어 checkbox input이 하나 있고 그것을 click하는 시나리오가 테스트 안에 있는 경우를 가정하겠습니다. 해당 input 엘리먼트가 존재는 하지만, css style 문제로 인해 유저 입장에서는 접근할 수 없는 위치에 있는 경우, 해당 click 이벤트는 실패하게 됩니다.
이것은 철저하게 개발자 입장이 아닌 유저 입장에서 테스트에 성공해야한다는 철학이 반영된 것입니다.
또한 cypress는 비개발자와의 좋은 협업 경험을 제공하기 위해 스크린샷과 비디오 기록(record) 기능을 제공합니다.
Cypress가 의존하는 모듈들
cypress는 내부적으로 mocha, chai, sinon, jQuery 등을 번들해서 사용하고 있습니다.
cypress 테스트 코드는 기본적으로 mocha 프레임워크 위에서 작성됩니다.
assertions 문법은 chai의 것을 사용하고 있습니다.
cy.stub()과 cy.spy() 함수의 경우는 sinon.js의 stub과 spy를 사용하고 있습니다.
DOM과 관련된 API는 jQuery의 것을 사용합니다.
Cypress로 e2e 테스트 하는 방식
cypress의 e2e 테스트는 BDD에 기반하여 다음과 같은 절차로 이루어집니다.
1. start server
우선 실제 서버가 열려있어야 합니다. 개발 서버가 열려있는 상태에서 cypress가 개발서버에 방문하여 브라우저를 띄우고 그 위에서 테스트를 하는 방식입니다. 만약 localhost:3000으로 프론트엔드 개발서버를 운영 중이라면, cypress test run 전에 3000번 서버를 먼저 열어야 합니다.
2. visit your server
cy.visit()는 브라우저 위에서 해당 주소에 방문하는 메서드입니다. 테스트 각 블록간의 의존성을 제거하기 위해 보통 beforeEach() 안에서 사용할 일이 많을 것 같습니다.
describe("example", () => {
beforeEach(() => {
cy.visit("http://localhost:3000);
});
});
3. user behavior
브라우저 위에서 유저가 행할 수 있는 행동들을 테스트 코드로 작성합니다. 유저는 UI를 통해 행동하므로 보통은 DOM query로 엘리먼트를 먼저 찾고, 그 엘리먼트에 이벤트를 가하는 형태로 액션을 취합니다.
예를 들어 checkbox input을 찾아서 click을 하는 액션은 다음과 같은 코드로 쓸 수 있습니다.
cy.get("input[type='checkbox']").click();
4. assertion
유저가 행동을 취한 후 기대되는 단언들을 확인합니다.
checkbox input을 click한 이후에 기대되는 단언은 input이 checked상태로 변하는 것입니다. DOM query이후 단언은 체이닝 메서드인 should를 이용하여 다음과 같이 확인할 수 있습니다.
cy.get("input[type='checkbox']").should("be.checked");
근데 액션에서 했던 DOM query와 중복되는 코드가 있습니다. 중복은 alias를 통해 제거할 수 있습니다. as 체이닝 메서드는 alias기능을 합니다.
cy.get("input[type='checkbox']").as("checkbox").click();
cy.get("@checkbox").should("be.checked");
BDD는 특성상 테스트 코드 한 블록 내의 코드가 긴 편입니다.
cypress는 element나 여러 변수들을 alias로 사용할 수 있어서, 재사용의 편리성을 제공해줍니다.
api server와 통신하는 앱의 테스트 방법
cypress 공식문서는 두가지 방향을 제시하고 있습니다.
1. 서버를 stub하지 않고 서버와 함께 진정한 의미의 e2e test를 하는 방향
2. 서버를 stub하여 서버와의 의존성이 없는 프론트엔드만의 e2e test를 하는 방향
1번의 경우 stub을 사용하지 않고 실제 백엔드 API 서버와 통신한 결과를 가지고 테스트를 수행합니다. 따라서 백엔드에 대해 의존성을 가지며, response에 대한 단언을 추가하면 백엔드에서 개발된 API까지 테스트가 가능해집니다.
2번의 경우는 stub에 관한 코드를 작성해줘야합니다. request가 발생했을 때 그것을 감지하는 코드와 그 후, stub response data를 뿌려주는 코드가 필요합니다. 이것은 cy.intercept() 메서드로 구현할 수 있습니다.
cy.intercept("GET", "/toDoList", request => {
request.reply({
// request.reply()는 응답데이터를 stub한다
fixture: "api/responseData/toDoList/get.json",
});
});
이 코드가 있는 테스트는 /toDoList 라는 엔드포인트로 get 요청이 발생하면 stub으로 미리 작성한 응답을 보내줍니다.
cypress는 seed data만 따로 fixture라는 폴더에 관리할 수 있는데, fixture 폴더에서 관리하는 데이터는 위 코드처럼 편리한 접근성을 가지게 됩니다
cy.* 메서드의 주요 특징
cypress는 가상 돔이 아닌 실제 돔 위에서 테스트를 수행합니다. 여러 브라우저의 종류와 여러 버전에서 테스트를 할 수 있기 때문에, 돔 쿼리는 jQuery의 DOM traversal method들을 사용합니다.
DOM query 이후 받은 element와 interact 하는 것은 체이닝 메서드들을 사용해서 작성할 수 있습니다. 다음과 같은 체이닝이 가능합니다.
cy.get("input").type("Hello!");
cy.get("input").click();
cypress가 jQuery를 번들했지만, jQuery와 다른 점은 DOM 쿼리를 성공할 때까지 retry 하다가 성공하면 다음 체이닝을 진행한다는 것입니다.
cypress 메서드들이 retry하는 이유는 비동기 로직들을 가진 테스트의 결과가 random 하게 되는 것을 방지하기 위해서입니다. 즉, cy.* 메서드들은 비동기적이고, retry하며, 체이닝이 됩니다.
cy.* 메서드들은 비동기적으로 실행되지만, cy.* 들이 연달아 있을 때 이들은 서로 이전 메서드의 실행이 끝나는 것을 큐에서 기다리며 순서대로 실행됩니다.
만약 이런 serial 실행이 보장되지 않으면 다음과 같은 테스트는 실패하게 될 것입니다.
cy.get("input[type='checkbox']").as("checkbox").click();
cy.get("@checkbox").should("be.checked"); // serial 하지 않으면 click 이전에 checked를 확인해서 테스트 실패
하지만 다행히도 실제로는 cy.*() 들이 serial하게 실행될 것이기 때문에, 첫 번째 줄이 다 끝나야 두 번째 줄이 실행될 것이므로 정상적으로 통과할 것입니다.
cypress를 사용하며 느낀 점
cypress를 사용하면서 개발 경험과 협업 경험 모두를 섬세하게 신경쓴 프레임워크라고 느꼈습니다. BDD와 TDD가 모두 가능하며 유비쿼터스 랭귀지로 직관적으로 읽히는 코드를 작성할 수 있었습니다. 또한 각 cy.* 메서드가 serial하게 실행되기에 비동기 로직이 있어도 테스트의 논리적 흐름을 쫓아가는 데에 어려움을 겪을 일이 없었습니다. configuration으로 다양한 사용자 환경을 테스트할 수도 있고, seed 데이터 관리도 편했고, stub이나 spy기능도 사용하기 쉬웠습니다. 프론트엔드 개발 중 e2e test의 필요성을 느낀다면 꼭 한번 도전해볼 가치가 있는 기술이라고 생각이 듭니다.
최근댓글