
이번 우테코 미션은 점심 머 먹지? 라는 간단한 음식점 추천 게시판을 만드는 것이었다. 이번 미션부터는 단위 테스트에서 벗어나, e2e 테스트를 도입했다. 원래는 jest를 써서 테스트를 진행했으나, 이번 미션부터는 cypress라는 개어려워보이는 이름의 테스트 도구를 활용했어야 했다. (jest를 제대로 쓴지 얼마나 됐다고..!!ㅠㅠ)
cypress를 쓰고 느낀 점은, 이렇게 프론트엔드 친화적이고 친절한 라이브러리가 있다니!!너무 좋다 였다.
항상 새로운 기술에 거부감이 들기 마련이지만, 최대한 좋은 점을 찾아 긍정적으로 접근하는 것이 중요한 것 같다.
우선 cypress로 진행한 e2e 테스트에 대해 알아본뒤, cypress를 정리하자!
e2e 테스트란 애플리케이션의 흐름을 처음부터 끝까지, 테스트 하는 것을 말한다. 유닛 테스트로 각 모듈이 정상적으로 동작하는지를 알 수 있었다. 하지만 실제 애플리케이션에서는 해당 모듈이 정상적으로 동작하지 않을 수도 있다.
프론트엔드 앱은 결국 '실제 사용자가 잘 사용할 수 있어야' 의미가 생긴다. E2E 테스트는 활용하면 실제 사용자가 앱을 사용하듯 나눠져 있는 기능들을 유의미한 기능 단위로 묶어 연속적으로 테스트한다. 이로 인해 실제 사용할 때 생길 수 있는 문제를 사전에 검증할 수 있다.
나는 이번 미션에서 cypress를 e2e 테스트를 중심으로 활용했다. 드디어 cypress에 대해 알아보자!
브라우저에서 직접 애플리케이션을 테스트하는 테스트 도구이다. 기존에 사용했던 jest와 가장 달랐던 부분이라고 느꼈다. 브라우저 환경에서 애플리케이션을 구동하는 프론트엔드에서는, 최대한 비슷한 환경에서 테스트를 돌릴 수 있다는 것이 큰 장점으로 느껴졌다.

cypress를 열면 처음 나오는 화면이다. 현재 컴퓨터에 깔린 브라우저를 자동으로 인식하여 테스트 환경을 만들어준다.

가장 왼쪽에는 파일별로 나뉜 테스트 파일 이름이, 중간에는 테스트의 목록이 나온다. cypress는 테스트 목록에 있는 테스트를 순서대로 실행시키고, 막혔을 경우 에러를 낸다.
오른쪽 부분은 테스트 환경의 브라우저 뷰인데, 실제로 사용자가 테스트 하는 것 같이 움직이는 뷰를 볼 수 있다(제일 재밌었던 부분😁)
cypress의 문법은 쉬운편이다. js를 기반으로 해서 js 문법을 활용할 수 있다. 메서드 명 또한 친절해서, 자동완성을 통해 원하는 메서드를 금방 찾을 수 있다.
다음은 미션에서 활용한 테스트 코드이다. 새음식점 추가 모달의 뷰를 테스트 해주었다.
describe('새 음식점 추가 모달 테스트', () => {
beforeEach(() => {
cy.visit('http://localhost:8080/');
});
it('처음 홈페이지를 음식점 모달이 보이지 않는다.', () => {
cy.get('#add-modal').should('not.be.visible');
});
it('헤더의 추가 버튼을 누르면 음식점 모달이 보인다.', () => {
const $addModalButton = cy.get('.gnb__button');
$addModalButton.click();
cy.get('#add-modal').should('be.visible');
});
그래도 우리가 지금까지 cypress에 대해 필요성을 못느꼈던 이유는, 서버를 켜서 직접 버튼을 클릭해보고, 눈으로 확인해보는 게 쉬워서라고 생각한다.(프론트엔드의 장점😋)
그럼에도 cypress를 써서 테스트를 하면 더 많은 장점을 누릴 수 있다. 예를 들어
물론 버튼 몇 개 딸깍 거리는 작업은 수작업으로 테스트하는게 빠르다. 하지만 새 음식점을 추가하기 위해 모든 데이터를 추가해야 한다면 어떨까? 만약 10개가 넘는 목데이터를 일일히 넣어줘야 한다면??😱
아마 코드를 완성하기도 전에 테스트를 하다가 지쳐버릴 것 같다.
cypress에는 fixture 라는 기능이 있다. 목데이터를
모킹 데이터를 많이 생성하는 작업에서 유용한 듯하다. 이번 미션에서는 음식점 데이터가 필요했고, 테스트를 하기 위한 데이터를 restaurantList.json 이라는 파일로 정리하였다. cypress 폴더에 fixtures 라는 폴더를 만들어 안에다가 목 데이터 파일을 넣어주었다. 이후 cy.fixture 라는 메서드를 활용하면 안에 들어있는 목데이터를 사용할 수 있다.
테스트를 할 때마다 실제 api를 호출한다면, 비효율적일 수도 있다. api를 호출할 때 실제로 호출하는 것이 아닌 이미 넣어논 데이터인 fixture를 반환받는 방법이 있다. 이를 보통 api 요청을 중간에 가로챈다고 해서 인터셉트(intercept)라고 부르는데, cypress도 이 인터셉트 기능을 제공한다.
beforeEach(() => {
cy.intercept(
{
method: 'GET',
url: /^https:\/\/api\/, //타겟이 될 api 주소를 적어준다.
},
{ fixture: 'movie-popular.json' }, //반환받고 싶은 fixture 파일
).as('getPopularMovies'); //as를 써서 별칭으로 지정한다.
위 코드는 api 주소로 요청을 보내면, cypress가 해당 api를 인터셉트 하여 fixture 안의 데이터를 반환한다. 이는 다음과 같이 사용할 수 있다.
cy.wait('@getPopularMovies').then(interception => {
const popularMovies = interception.response.body.results;
expect(popularMovies.length).to.equal(20);
}
getPopularMovies이 api를 부르는 비동기 작업이기 때문에 해당 코드가 프로미스를 반환하고, 위에서 사용된 then 메서드 또한 프로미스와 관련되었다고 오해할 수 있으나, 이건 사실이 아니다. then은 그저 getPopularMovies의 결과값에 대해 다음에 수행할 것을 쓸 수 있는 기능이다.
참고 : Cypress .then은 프로미스가 아니다.
위는 내가 음식점 추가 모달에서 진행한 e2e 테스트이다.
꽤나 많아 보이지만, 모두 유저가 해당 컴포넌트를 이용하면서 발생시킬 수 있는 이벤트라고 생각해서 모두 테스트 해주었다.
테스트 제목을 자세히 적어주니까, 유저가 음식점을 추가한다 라는 이벤트와 관련해서 유저의 사용 플로우가 정리된 느낌이었다. 테스트 코드도 잘 정리하면 좋은 문서가 된다는 점을 느끼게 되었다.
사실 이는 cypress의 장점이라기 보다는, 테스트 코드에서 중요한 것이라고 생각한다.
한 테스트 내에서도 테스트를 맥락에 따라 분리하고 싶을 때가 있다. 이 때 context를 사용하면 정말 좋다.
context('뷰포트가 660이상 일 때', () => {
it('인풋이 열려있다.', () => {
cy.get('#search-input').should('have.class', 'open');
cy.get('#search-input').should('is.visible');
});
it('버튼이 검색 기능을 한다.', () => {
cy.get('#search-button').click();
cy.get('.toast-message').should('be.visible');
});
});
다음과 같이 테스트를 실행하면, context로 구분된 것이 표시된다.👍

사실 e2e 테스트를 처음하다보니, 테스트의 범위를 어디까지 해야할지? 에 대해 고민이 많았다. 모든 컴포넌트의 동작과, 예외사항을 테스트했지만, 정확한 기준이 없었다.
리뷰어인 피터는 '테스트도 비용이니, 모든 것을 테스트하는 것 보다는, 중요 기능을 중심으로 테스트를 작성하고, 너무 사소해서 깨지는 테스트는 과감하게 버려도 된다. 테스트의 비용과 테스트의 유용함 사이에서 밸런스를 맞춰야 한다'는 의견을 주었다.
그리고 테스트를 생각하면서 BDD (Behavior Driven Development)와 관련된 글을 찾아봤는데, 다음과 같은 내용이 인상적으로 읽혔다!
테스트 대상이 가지고 있는 인터페이스에 대한 테스트 시나리오가 중요한 것이지 내부적으로 사용되는 모든 함수가 명시적인 테스트 대상은 아니다. 인터페이스가 변하지 않는 이상 내부적인 리펙토링을 거친 코드는 테스트 코드 수정없이 모두 통과되어야 한다.!
위 말은 너무 작은 단위의 테스트 코드보다는, 유저가 일으킬 수 있는 적당한 기능 단위를 기반으로 테스트 코드를 짜야 유용하다는 의견같다. 사실 테스트 코드의 목적이 세부적으로 프로덕션 코드를 100% 커버하나?가 아니라, 유저 입장에서 그 애플리케이션이 정상적으로 굴러가는지? 가 쟁점이니 말이다.
두 분의 의견을 읽고, 테스트는 유저 입장에서 생각하고 짜야한다는 것을 배웠다. 유저가 웹페이지에 들어가서 할 행동들, 즉 유저 플로우를 고려하여 테스트를 짜야 할 것 같다.
테스트 코드에서 유저 플로우가 잘 느껴진다면 BDD를 보여주는 좋은 문서가 될 것이다.
또한 테스트의 비용을 고려해서, 지나치게 작은 범위의 e2e 테스트는 지양해야 한다는 점을 배웠다. 플로우 단위로 함수 범위의 작은 테스트는 e2e 랑은 거리가 먼 것 같다!
앞으로 이런 점들을 고려해서 테스트 코드를 짜야겠다.
덕분에 e2e 테스트에 대해 정리가 되었습니다 👍 저도 e2e 테스트의 범위에 대해 고민이 많았는데, 기능 요구 사항을 하나의 테스트로 잡는 것도 괜찮은 것 같아요!