Notice
Recent Posts
Recent Comments
Link
관리 메뉴

윤일무이

[Test Code] Cypress로 본격적인 테스트 코드 작성하기(fixture, recoil mocking) 본문

⚛️ React/📜 Test Code

[Test Code] Cypress로 본격적인 테스트 코드 작성하기(fixture, recoil mocking)

썸머몽 2024. 7. 2. 18:45
728x90

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>
      ))}

 

 

 

Testing | Recoil

Testing Recoil state inside of a React component

recoiljs.org

 

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해서 테스트를 진행해보았다.

 


 

참고 강의

 

2시간으로 끝내는 프론트엔드 테스트 기본기 강의 | 강병진 - 인프런

강병진 | 테스트코드! 어디서부터 시작해야할지 막막한 분들을 위해 준비했어요. 테스트 작성부터, 자동화를 통한 배포까지 한번에!, 테스트 코드를 작성하고 싶은 프론트엔드 개발자를 위한 강

www.inflearn.com

 

728x90