Cypress로 공통 컴포넌트 테스트 하기
주문을 테스트 하려고 한다.
이 때 사용자는 배달/주문 중 원하는 유형을 하나 선택할 수 있다.
/ 페이지에 오면 배달 버튼과 포장 버튼이 보인다.
배달 버튼을 클릭하면 /food-type으로 이동하면서 여러 음식 종류가 노출되어야 한다.
describe('주문을 테스트 한다', () => {
it('사용자는 배달/주문 중 원하는 유형을 선택할 수 있다', () => {
cy.visit('/');
cy.get('[data-cy=deliveryBtn]').should('be.visible').as('deliveryBtn');
cy.get('[data-cy=pickupBtn]').should('be.visible').as('pickupBtn');
cy.get('@deliveryBtn').click();
cy.url().should('include', '/food-type');
});
});
테스트는 이렇게 작성된다.
이때 공통 컴포넌트 부분을 다시 봐보자.
// OrderTypePage.tsx
<OrderType
handleOrderTypeClick={handleDeliveryBtnClick}
icon="ic-delivery.png"
orderType="delivery"
testId="deliveryBtn"
/>
// OrderType.tsx
export default function OrderType({
orderType,
icon,
handleOrderTypeClick,
testId,
}: IOrderType) {
return (
<OrderTypeBtn onClick={handleOrderTypeClick} data-cy={testId}>
<img
width={40}
height={40}
src={`https://kr.object.ncloudstorage.com/icons/${icon}`}
/>
{orderCategory[orderType]}
</OrderTypeBtn>
);
}
컴포넌트를 보면 data-cy를 사용하지 않고 testId를 대신 사용한다.
이유는 OrderType이 JSX 태그가 아닌 그냥 함수(컴포넌트)이기 때문에 data-cy를 읽어오지 못한다.
tag에 넣어줘야 하기 때문에 testId를 사용해 prop으로 넘겨줘서 사용하게 됐다.
배달 버튼을 눌렀을 때 /food-type으로 이동한 게 테스트의 마지막 화면이 된다.
Cypress interceptor를 활용한 HTTP request mocking
마지막 화면까지 오면 /restaurant/food-type으로 get 요청을 보내게 된다.
[
{
"id": 1,
"name": "피자",
"icon": "https://kr.object.ncloudstorage.com/icons/ic-pizza.png"
},
{
"id": 2,
"name": "동남아",
"icon": "https://kr.object.ncloudstorage.com/icons/ic-asian.png"
},
{
"id": 3,
"name": "햄버거",
"icon": "https://kr.object.ncloudstorage.com/icons/ic-burger.png"
},
...
]
이런 식으로 response가 오게 된다.
로그인 때와 같이 mocking을 해보자.
describe('주문을 테스트 한다', () => {
it('사용자는 배달/주문 중 원하는 유형을 선택할 수 있다', () => {
cy.visit('/');
cy.get('[data-cy=deliveryBtn]').should('be.visible').as('deliveryBtn');
cy.get('[data-cy=pickupBtn]').should('be.visible').as('pickupBtn');
cy.get('@deliveryBtn').click();
cy.url().should('include', '/food-type');
});
it('사용자는 음식 종류를 선택할 수 있다', () => {
cy.visit('food-type');
cy.intercept(
{
method: 'GET',
url: '/restaurant/food-type',
},
[
{
id: 1,
name: '피자',
icon: 'https://kr.object.ncloudstorage.com/icons/ic-pizza.png',
},
{
id: 2,
name: '동남아',
icon: 'https://kr.object.ncloudstorage.com/icons/ic-asian.png',
},
...
]
);
cy.get('[data-cy=1]').should('be.visible').as('pizzaBtn');
cy.get('@pizzaBtn').click();
cy.url().should('include', '/food-type/1');
});
});
url이 /restaurant/food-type인 GET 요청이 들어오면 인터셉트 해서 response들을 보내준다.
FoodTypePage의 FoodType 버튼들은 data-cy가 foodType의 id를 갖고 있다.
id: 1은 피자이기 때문에 피자 버튼이 나와야 하고, 그것을 클릭했을 때 /food-type/1로 이동한다.
잘 이동해서 피자 종류들이 나오고 있는 걸 확인할 수 있다.
Cypress에서 fixture 활용
fixture란 cypress에서 여러 곳에서 사용되는 고정 값을 뜻한다.
지금 인터셉트를 할 때 response들을 모두 가져오는데 이게 10개일 때도 있고 99개일 때도 있다.
이걸 모두 가져오면 테스트 코드가 길어져 가독성에도 좋지 않고, 나중에 서버랑 소통할 때도 헷갈릴 수 있어서
js, ts의 상수 개념과 유사한 fixture를 사용해 response 값들을 고정해보려고 한다.
cypress 안에 이미 fixtures 폴더가 생성되어 있다.
restaurant-list.json을 생성한다.
// restaurant-list.json
[
{
"id": 1,
"name": "Quae.피자",
"ratings": 4.75,
"minPrice": 21000,
"icon": "https://kr.object.ncloudstorage.com/icons/ic-pizza.png"
},
{
"id": 2,
"name": "Nam.피자",
"ratings": 3.96,
"minPrice": 34000,
"icon": "https://kr.object.ncloudstorage.com/icons/ic-pizza.png"
},
{
"id": 3,
"name": "Quia.피자",
"ratings": 3.3,
"minPrice": 30000,
"icon": "https://kr.object.ncloudstorage.com/icons/ic-pizza.png"
},
{
"id": 4,
"name": "Quae.피자",
"ratings": 1.2,
"minPrice": 30000,
"icon": "https://kr.object.ncloudstorage.com/icons/ic-pizza.png"
},
{
"id": 5,
"name": "Ea.피자",
"ratings": 1.83,
"minPrice": 12000,
"icon": "https://kr.object.ncloudstorage.com/icons/ic-pizza.png"
}]
이제 intercept를 할 때 response 값을 직접 입력하지 않아도 되고 { fixture: "json파일 이름" }을 넣어주면 된다.
RestaurantBtn에는 restaurant.id가 들어 있는데 이걸 보고 클릭해서 상세 메뉴로 들어가게 된다.
fixture를 사용해서도 들어갈 수 있는데, fixture의 특정 값을 클릭하라고 지시할 수 있다.
it('사용자는 원하는 레스토랑을 선택할 수 있다', () => {
cy.visit('/food-type/1');
cy.intercept(
{
method: 'GET',
url: '/restaurant/food-type/1',
},
{ fixture: 'restaurant-list.json' }
);
cy.fixture('restaurant-list.json').then(restaurant => {
cy.get(`[data-cy=${restaurant[0].id}]`)
.should('be.visible')
.as('restaurantBtn');
cy.get('@restaurantBtn').click();
cy.url().should('include', '/restaurant/1');
});
});
이렇게 코드가 깔끔해졌다!
1번인 Quae.피자의 상세 페이지로 들어오게 되었다.
이제 장바구니에 아이템을 담고 주문을 해보자 🍕
Recoil 테스트
장바구니에 아이템을 담고 주문을 하기 위해 OrderDetailPage에 가보자.
총 가격, 레스토랑, 주문을 모두 recoil을 사용해 전역 변수로 관리하고 있다.
증가 버튼을 누르면 1씩 추가, 감소 버튼을 누르면 1씩 감소된다.
// OrderDetailPage.tsx
const totalPrice = useRecoilValue(totalPriceState);
const restaurant = useRecoilValue(targetRestaurantState);
const [newOrder, changeCount] = useRecoilState(newOrderState);
...
const handleIncrementBtnClick = (menuId: number) => {
changeCount((oldArray) =>
oldArray.map((item) =>
item.id === menuId ? { ...item, count: item.count + 1 } : item
)
);
};
...
{newOrder.map((menu) => (
<MenuWrap>
<img alt={menu.name} src={menu.picture} width={100} height={100} />
<MenuInfo>
<MenuName>{menu.name}</MenuName>
<MenuPrice>{`${menu.price.toLocaleString()}원`}</MenuPrice>
<CounterSection data-cy="counter">
<DecrementBtn
data-cy="decrementBtn"
onClick={() => handleDecrementBtnClick(menu.id)}
>
-
</DecrementBtn>
{menu.count}
<IncrementBtn
data-cy="incrementBtn"
onClick={() => handleIncrementBtnClick(menu.id)}
>
+
</IncrementBtn>
</CounterSection>
</MenuInfo>
</MenuWrap>
))}
recoil 공식 문서에서 테스트에 대해 작성한 부분이 있다.
recoil에서는 특정 값에 직접 접근하는 게 아니라 onChange 이벤트를 발생 시킨다.
RecoilState에 있는 특정 값을 바로 호출하지 않고 event를 발생 시키는 게 테스트 방법이다.
그래서 recoil 값을 변경 시키는 2가지 함수(증가, 감수)를 사용해서 장바구니를 처리해보자.
먼저 menu.json을 만들어서 이 테스트에서 사용할 fixture를 만들어준다.
// menu.json
{
"id": 1,
"menu_set": [
{
"id": 1,
"name": "페퍼로니피자",
"description": "올타임 베스트 근본피자",
"price": 18000,
"picture": "https://kr.object.ncloudstorage.com/icons/pepperoni-pizza.jpeg",
"restaurant": 1
},
{
"id": 2,
"name": "프로슈토 피자",
"description": "살짝 있어보이는 느낌",
"price": 21000,
"picture": "https://kr.object.ncloudstorage.com/icons/prosciutto-pizza.jpeg",
"restaurant": 1
},
{
"id": 3,
"name": "트러플 머시룸 피자",
"description": "고급진 트러플향이 뿜뿜",
"price": 17000,
"picture": "https://kr.object.ncloudstorage.com/icons/mushroom-pizza.jpeg",
"restaurant": 1
},
...
],
}
테스트를 할 때 cy.visit('/order')를 바로 해버리면 처음에 recoil 값이 없기 때문에, 바로 위에서 진행하던 테스트에 이어서 작성한다.
it('사용자는 원하는 메뉴를 장바구니에 담고 원하는 개수로 변경할 수 있다.', () => {
cy.visit('/restaurant/1');
cy.intercept(
{
method: 'GET',
url: '/restaurant/1',
},
{
fixture: 'menu.json',
}
);
/restaurant/1로 GET 요청을 보내면 인터셉트 해서 fixture를 보여준다.
cy.fixture('menu.json').then(menu => {
cy.get(`[data-cy=${menu.menu_set[0].id}]`)
.should('be.visible')
.as('foodBtn');
cy.get('@foodBtn').click();
cy.url().should('include', '/order');
cy.get('[data-cy=counter]').as('counter');
cy.get('@counter').should('contain', 1);
cy.get('[data-cy=incrementBtn]').should('be.visible').click();
cy.get('@counter').should('contain', 2);
cy.get('[data-cy=decrementBtn]').should('be.visible').click();
cy.get('@counter').should('contain', 1);
cy.get('[data-cy=completeBtn]').should('be.visible').click();
cy.url().should('include', '/');
fixture를 받고, menu_set[0].id인 페퍼로니 피자에 접근해보자.
페퍼로니 피자 버튼이 보이는지 확인하고, 이 버튼을 foodBtn이라고 하자.
버튼을 클릭하면 /order로 이동한다.
카운터 버튼의 값은 1이어야 한다.
data-cy=counter를 찾고 이를 counter라는 별칭으로 지정한 후 contain이 1인지 확인한다.
이전에는 include로 확인했었는데 section으로 되어 있는 태그는 contain으로 확인해줘야 한다.
다음 증가 버튼(data-cy=incrementBtn)이 보이고, 이를 클릭하면 @counter가 2를 contain한다.
감소 버튼(data-cy=decrementBtn)이 보이고, 이를 클릭하면 @counter가 1을 contain한다.
주문 완료 버튼(data-cy=completeBtn)이 보이고, 이를 클릭하면 url이 /를 include한다!
이렇게 recoil까지 mocking해서 테스트를 진행해보았다.
참고 강의
'⚛️ React > 📜 Test Code' 카테고리의 다른 글
[Test Code] cypress cloud / aws amplify에서 테스트 진행하기 (0) | 2024.07.16 |
---|---|
[Test Code] 스토리북 사용법 (0) | 2024.07.09 |
[Test Code] Cypress로 테스트 하기 (0) | 2024.07.02 |
[Test Code] react-query 공식 문서를 따라 테스트 코드를 하면 안되는 이유 + nock으로 mocking 하기 (0) | 2024.06.26 |
[Test Code] beforeEach()를 활용한 Jest 성공 케이스 작성 (0) | 2024.06.19 |