문벅스 카페메뉴 만들기 - 블랙커피JS 레벨1 13기를 간단하게 회고하고,
문벅스 카페메뉴 만들기 과제에 Cypress를 이용하여 적용하면서 배운 내용 및 적용 과정을 서술한 글입니다.
2/21일 NEXTSTEP에서 진행하는 문벅스 카페메뉴 만들기 - 블랙커피JS 레벨1 13기가 종료되었습니다. 처음에 이 NEXTSTEP이 정확히 무슨일을 하는 곳인지 몰랐는데, 우아한 테크코스의 운영진분들의 양질의 교육을 받을 수 있다고 하여 프론트와 관련해 블랙커피 스터디📖 에 참여하게 되었습니다.
매주 월요일 8시에 게더타운에서 모여 교육을 진행하였는데, 매주 미션을 진행하면서 팀원들의 피드백과 코드리뷰를 받으면서 성장할 수 있었던 좋은 시간이었습니다. 👍
마지막 날의 목표는 TDD의 핵심 철학인 문제 해결 과정에서의 '피드백'에 대해 이해하고 Cypress를 활용하여 E2E 테스트 코드를 페어로 함께 작성해보는 것이었습니다.
이를 위해 TDD가 무엇인지 설명해주셨는데, 그 내용은 아래와 같습니다.
창시자: 켄트 백
정의: 테스트를 먼저 만들고, 테스트를 통과하기 위한 코드를 짜는 개발 방법
예시: 생년월일을 입력받아 나이를 출력해주는 프로그램
test("생년월일을 입력받아 나이를 출력한다. ", {
// 2022년 한국나이 기준
expect(함수(19970919)).toBe(26)
})
// 위와 같이 먼저 테스트 코드를 작성하고, 함수를 만든 뒤 다시 테스트를 한다.
const getAgeUsingDateofBirth () = { ... }
test("생년월일을 입력받아 나이를 출력한다. ", {
// 2022년 한국나이 기준
expect(getAgeUsingDateofBirth(19970919)).toBe(26)
})
목적: 테스트를 먼저 만들고, 실제 코드를 작성하고, 원하는 대로 동작하는지 빠르게 피드백을 받는 것
→ 주니어가 성장하는 좋은 방법또한 코드를 작성하고 피드백을 받는 코드리뷰와 같이 빠른 피드백을 받는 것이 중요하다!
핵심: 더 자주, 더 빨리, 더 꾸준하게 ‘피드백’을 받는 것
결정과 피드백 사이의 갭에 대한 인식, 결정과 피드백 사이의 갭을 조절하기 위한 테크닉
→ 결정: 내가 시도하려는 것, 피드백: 성공/실패
결정과 피드백 사이의 갭을 줄이는 방법으로 학습 전략을 이어나가자.
→ 주니어 개발자들이 “인터넷 강의를 보며 이론적인건 이해를 해도 실제 개발을 하려고 하면 뭐부터 해야할지 모르겠어요"라는 질문을 많이 하는데, TDD의 핵심 철학처럼 내가 해결할 수 있는 가장 작은 영역의 문제를 풀어나가면서 피드백을 받는 것처럼 결정(기능구현)과 피드백(리뷰)을 줄이는 방법으로 학습 전략을 이어나가자는 의미
TDD에 대한 설명을 듣고, 팀원들과 미션 과제에 몇몇 기능에 대해 Cypress를 적용하면서 사용해보았는데 작성한 테스트코드를 시각적으로 어떻게 테스트가 되는지, 정상적으로 작동이 되는지 보면서 매우 신기했습니다.
이후, Cypress에 대해 더 공부하고, 나머지 기능들에도 테스트를 해보고 싶어 개인 미션 코드에 적용해보기로 결심했습니다.
이미 기능들이 만들어져 있어 TDD 방법론은 아니지만, 구현된 기능을 테스팅하기 편리할 것 같아 Cypress 테스트코드를 작성해 테스트를 자동화 해봅시다.
우선 초기환경을 다음과 같이 설정해 줍시다.
moonbucks.spec.js
describe('example to-do app', () => {
beforeEach(() => {
cy.visit('../../index.html');
});
});
위와 같이 설정하게 되면 테스트를 할때마다, index.html로 들어가 테스트를 진행하게 됩니다.
문벅스의 기능인 에스프레소 메뉴에 새로운 메뉴를 확인 버튼 또는 엔터키 입력으로 추가한다.
를 테스트 코드로 작성해 보겠습니다.
it('에스프레소 메뉴판에 새로운 메뉴를 확인 버튼을 눌러 추가할 수 있다.', () => {
const newMenu = '아메리카노';
cy.get('#menu-name').type(newMenu);
cy.get('#menu-submit-button').click();
cy.get('#menu-list li').contains(newMenu).should('be.visible');
});
it('에스프레소 메뉴판에 새로운 메뉴를 엔터 키를 눌러 추가할 수 있다.', () => {
const newMenu = '카페모카';
cy.get('#menu-name').type(`${newMenu}{enter}`);
cy.get('#menu-list li')
.should('have.length', 2)
.last()
.should($li => {
expect($li).to.contain(newMenu);
});
});
대부분의 메서드들은 cypress api docs에서 확인할 수 있었는데,
type
을 통해 input에 값을 입력하고,should
를 통해 값이 어떠한 상태인지 확인할 수 있었습니다.
다음 기능으로 메뉴가 추가되고 나면, input은 빈 값으로 초기화한다.
가 있었는데 이 부분은 테스트코드로 다음과 같이 작성할 수 있습니다.
it('메뉴가 추가되고 나면 input은 빈 값으로 초기화한다', () => {
const newMenu = '카페라떼';
cy.get('#menu-name').type(`${newMenu}{enter}`);
cy.get('#menu-name').should('have.value', '');
});
마찬가지로
should
메서드를 사용해 input의 value를 확인하여 ''(빈 값)인지 테스트합니다.
사용자 입력값이 빈 값이라면 추가되지 않는다.
기능의 테스트코드는 조건 처리가 필요하여 아래와 같이 작성하였습니다.
it('사용자 입력값이 빈 값이라면 추가되지 않는다.', () => {
cy.get('#menu-name').then($input => {
if ($input.text() === '') {
cy.get('#menu-submit-button').click();
cy.get('#menu-list li').should('have.length', 3);
}
});
});
인풋 요소를 가져온다음, 체이닝으로 input요소의 text값이 빈값이라면 메뉴 추가 버튼을 눌러도 추가 되지 않아야 합니다. 이를 위해 기존 배열의 length값을 변수로 가져오는 방법을 찾아보았는데 잘 나오지 않아 기존 갯수인 3으로 비교하였습니다.
메뉴의 수정 버튼을 눌러 메뉴 이름 수정할 수 있다.
기능과 메뉴 삭제 버튼을 이용하여 메뉴 삭제할 수 있다.
는 둘다 window.prompt
와 window.confirm
API를 사용하기 때문에 묶어서 보여드리겠습니다.
it('메뉴의 수정 버튼을 눌러 메뉴 이름을 수정할 수 있다.', () => {
const newMenuName = '아이스 아메리카노';
cy.window().then(win => {
cy.stub(win, 'prompt').returns(newMenuName);
});
cy.get('.menu-edit-button').first().click();
cy.get('#menu-list li').first().contains(newMenuName);
});
it('메뉴의 삭제 버튼을 눌러 메뉴를 삭제할 수 있다.', () => {
cy.window().then(() => {
cy.on('window:confirm', () => true);
});
cy.get('.menu-remove-button').first().click();
cy.get('#menu-list li').should('have.length', 2);
});
window prompt
api와 관련해서 cypress에서 테스트를 하기 위해서는stub
메서드를 사용해야 합니다.
prompt 창이 열렸을때, 반환해줄 값을 넣어두고 prompt 창을 호출할 수정 버튼을 가져와 클릭하면 자동 테스트가 진행됩니다.
삭제버튼의 경우 위와 같이cy.on
을 사용하여 'window:confirm' 을 첫번째 인수로주고,true
혹은false
를 반환하게하여 테스트를 진행할 수 있습니다.
총 메뉴 갯수를 count하여 상단에 보여준다.
는 간단하게 textContent를 가져와 비교하였습니다.
it('총 메뉴 갯수를 count하여 상단에 보여준다.', () => {
cy.get('.menu-count').should('have.text', '총 2개');
});
종류별로 메뉴판을 관리
기능과 페이지에 최초로 접근할 때는 에스프레소 메뉴가 먼저 보이게 한다.
기능은 아래와 같이 테스트 코드를 작성하였습니다.
// constants/constants.js
export const KOREAN_MENU_NAME = Object.freeze({
espresso: '☕ 에스프레소',
frappuccino: '🥤 프라푸치노',
blended: '🍹 블렌디드',
teavana: '🫖 티바나',
dessert: '🍰 디저트',
});
// moonbucks.spec.js
it('종류별 메뉴판 관리할 수 있게 만든다.', () => {
const menuNames = ['espresso', 'frappuccino', 'blended', 'teavana', 'dessert'];
menuNames.forEach(menuName => {
cy.get(`[data-category-name=${menuName}]`).click();
cy.get('[data-component=menu-header]')
.children()
.first()
.contains(KOREAN_MENU_NAME[menuName]);
});
});
it('페이지에 최초로 접근할 때는 에스프레소 메뉴가 먼저 보이게 한다.', () => {
cy.get('[data-component=menu-header]')
.children()
.first()
.contains(KOREAN_MENU_NAME['espresso']);
});
반복문을 돌면서 nav 버튼을 클릭했을때, 그에 맞는 메뉴판의 text가 나오도록 테스트를 진행하였습니다.
마지막 품절 상태인 경우를 보여줄 수 있게, 품절 버튼을 추가하고 sold-out class를 추가하여 상태를 변경한다.
기능입니다.
it('품절 상태인 경우를 보여줄 수 있게, 품절 버튼을 추가하고 sold-out class를 추가하여 상태를 변경한다.', () => {
cy.get('.menu-sold-out-button').first().click();
cy.get('#menu-list li').children().first().should('have.class', 'sold-out');
});
should
의have.class
를 가지고sold-out
클래스가 존재하는지 비교합니다.
깃헙 코드: https://github.com/hustle-dev/moonbucks-menu
이미 완성된 기능들의 테스트 코드를 작성하면서도 어떤 방식으로 테스트하고, 어떻게 테스트 코드를 작성해야 할지 고민되었습니다. 또한 cypress를 처음 사용하다 보니 메서드에 익숙지 않아 테스트 코드 작성 시, 어떤 메서드를 활용하는 것이 적절한지 아직 미숙하였던 것 같습니다.
그러나 테스트 코드를 하나하나 작성해보고, 기능들이 만들어져 있지 않다는 가정하에 구현해야 하는 기능들을 분리하면서 생각하다 보니 어떤 방식으로 기능들을 구현해 나가야 할지 조금조금 보이는 것 같았습니다. 또한 TDD 방법론을 실제 프로젝트에 적용해보진 않았지만, 이런 방법을 통해 기능을 작은 것부터 구현해 나가는 데 도움이 될 것 같다는 느낌을 받았습니다.
가장 좋았던 것은 기능을 만들고, 잘 돌아가는지 테스트를 해볼 필요가 없다는 것이었습니다. 자동화된 E2E 테스트를 시각적으로 볼 수 있었던 것도 재미있는 경험이었습니다. 앞으로 프로젝트 진행 시 cypress와 같은 테스트 도구들을 적극적으로 활용하여 코드의 신뢰성을 높이면서 개발해보고 싶다는 생각이 들었고, 유용한 툴을 알게 되어 값진 시간이 되었습니다.
ㄷ ㄷ 잘쓰셨네요