<OrderPage.js>
이 페이지의 Products (여행 상품) Options (여행 옵션)들은 백엔드 서버에서 가져온다. 그렇다면 이러한 부분은 어떠게 테스팅을 해줘야 할까?
1. msw 설치
npm install msw
2. 핸들러 생성
<src/mocks/handlers.js>
<src/mocks/server.js>
<src/setupTest.js>
💡 참고하기 좋은 자료
mock service worker를 이용해서 products를 테스트해보자.
<Type.test.js>
<Type.js>
<Product.js>
1. 전달 받은 imagePath를 img의 경로로 주어 이미지를 불러오게 된다.
2. 해당 이미지의 이름과 상품 갯수를 측정하는 input을 작성해준다.
서버에서 데이터를 가져올 때 에러가 발생하면 에러창을 표시하자.
<Type.test.js>
<components/errorBanner.js>
<Type.js>
mock service worker를 이용해서 products를 테스트해보자.
<Type.test.js>
💡 잠깐) 앞서 products의 갯수와 옵션의 갯수가 2개인지를 확인하기 위한 테스트 케이스를 작성하였다. 실제로 서버에 담긴 Products는 4개이며, Options는 3개이다. 그렇다면 테스트케이스에서 작성한 2개는 무엇일까? 바로 우리가 앞서 작성해놓은 MSW Handler에 작성된 코드이다. 앞서 말했듯이 Hanlder로 요청을 조작하여 모의응답을 준 것이다. 다음과 같이 Handler에 작성된 갯수는 각각 2개씩이다.
💡 총 가격에 대한 구현은 OrderContext를 확인하자!
<Type.js>
💡 잠깐) 공톤된 로직의 중복을 제거한, 삼항연산자 컴포넌트
앞서 보았듯이 OrderPage안에는 세개의 컴포넌트가 존재한다. 일반적으로 작성했으면 두개의 컴포넌트가 존재했을 것이다. 바로 Proucts와 Options 컴포넌트인데,Why? Type컴포넌트의 역할은 무엇일까?
곰곰히 생각해보니 Products와 Options는 서버에서 데이터를 받아와서 출력하게 된다. 또한 각각 갯수에 맞는 total price를 구현하게 될 것인데 이때 각각 컴포넌트의 값들을 context API를 통해 전달하게 된다.So, 코드의 중복이 발생한다. 데이터 통신에 대한 함수와 context를 받아오는 코드 등 많은 로직 부분들을 Products 컴포넌트에 한번, Options 컴포넌트에 한번 따로따로 작성하게 된다.
Finally, type 컴포넌트의 역할은 상위 컴포넌트인 OrderPage에서 전달하는 orderType에 따라서 컴포넌트를 띄워주는 형식이고, Type 컴포넌트 안에 앞서 말했던 로직들을 한번만 작성하면 되는 것이다.
🧐 이 것이 HOC(고차 컴포넌트)와 관련된 작성법인가 공부해야겠다.
중복 O
<OrderPage> <Products /> // 각자 따로 데이터 통신 & contextAPI & props 전달 <Options /> // 각자 따로 데이터 통신 & contextAPI & props 전달 </OrderPage>
중복 X
<OrderPage> <Type orderType="Products" /> <Type orderType="Products" /> </OrderPage> <Type> // 한번에 데이터 통신 & contextAPI & props 전달 const ItemComponents = orderType === "products" ? Products : Options; </Type>
<Option.js>
이제 상품 가격을 구현해보자.products 하나씩에 대해 수량을 선택할 수 있고, 각자 수량에 대한 금액을 출력해보자. 또한 Options의 체크 항목에 따라서도 금액이 출력되게 구현해 보고 Products와 Options의 총 금액을 구해보자.
그렇다면 각각 컴포넌트에서 금액은 어떻게 구할 것이며, 각각의 값들을 어떻게 더하여 총 금액을 출력할까?
1. Context 생성
2. Context의 값을 공유해줄 Provider 생성(Context는 Provider안에서 사용가능)
3. Provider를 return 해주는 OrderContextProvider를 생성.
4. value로 넣어줄 데이터를 만들기
필요 데이터 양식: orderCounts의 products, options에는 수량을 증가시킨 상품 또는 체크한 옵션들이 Map()을 통해 각각 배열에 담기게 된다.
(Map(): 간단한 키와 값을 서로 매핑시켜 저장하며 저장된 순서대로 각 요소들을 반복적으로 접근)
데이터 업데이트: 그리고 그 갯수에 따라서 각각의 가격과 총 가격을 구할 수 있다.
updateItemCount는 총 세개의 매개변수(parameter)를 받는다.(전달될 인자값은 이 함수를 공유할 컴포넌트들로부터 온다.)
itemName: 상품 또는 옵션 중 어떤 이름은 가진 아이템인지를 구분하기 위해 인자값(argument)을 받는다.
newItemCount: 그 아이템의 갯수가 몇개인지 인자값을 받는다.
orderType: 그 아이템이 products인지 options인지 인자값을 받는다.
orderCounts를 spread연산자를 통해 복사하여 newOrderCounts에 저장한다. 즉, newOrderCounts의 값이 변함에 따라 orderCounts 값을 변경시킨다.
const newOrderCounts = { ...orderCounts };
const orderCountsMap = orderCounts[orderType];
orderCountsMap.set(itemName, parseInt(newItemCount));
setOrderCounts(newOrderCounts);
return [{ ...orderCounts}, updateItemCount];
💡 중요 ! value를 useMemo로 감싸준 이유?
value 값을 공유하는 컴포넌트들이 렌더링할때 마다 값을 공유하는 모든 컴포넌트들이 같이 렌더링 되어진다. 따라서, useMemo의 deps로 orderCounts를 준다. 그러면 orderCounts의 변경에만 렌더링되어진다.
const [totals, setTotals] = useState({
products: 0,
options: 0,
total: 0
})
useEffect(()=>{
const productsTotal = calculatesSubtotal('products', orderCounts);
const optionsTotal = calculatesSubtotal('options', orderCounts);
const total = productsTotal + optionsTotal;
setTotals({
products: productsTotal,
options: optionsTotal,
total: total
})
},[orderCounts])
const pricePerItem = {
products: 1000,
options: 500
}
function calculatesSubtotal(orderType, orderCounts){
let optionCount = 0;
for( const count of orderCounts[orderType].values()){
optionCount += count;
}
return optionCount * pricePerItem[orderType];
}
return [{ ...orderCounts, totals }, updateItemCount];
products 와 options 컴포넌트에 공유되는 공통된 로직을 가진 Type.js에 useContext을 이용하여 OrderContext를 받아온다. 즉, OrderContextProvider를 통해 공유되는 값들을 가져온다.
<OrderContext.js>
return [{ ...orderCounts, totals }, updateItemCount];
<Type.js>
import OrderContext from "../../context/OrderContext";
const [orderDatas, updateItemCount] = useContext(OrderContext)
const optionItems = items && items.map((item) => (
<ItemComponents
key={item.name}
name={item.name}
imagePath={item.imagePath}
updateItemCount={(itemName, newItemCount) =>
updateItemCount(itemName, newItemCount, orderType)
}
/>
));
<p> 총 가격:{orderDatas.totals[orderType]}</p>
const [totals, setTotals] = useState({
products: 0,
options: 0,
total: 0
})
ex) Type.js에서 OrderPage로 부터 'products'인 orderType을 props로 받았다. 따라서 totals['products']가 되어, products의 총 가격을 받게 된다.
function OrderPage({ setStep }) {
const [orderDatas] = useContext(OrderContext);
return (
<div>
<h1>Travel Products</h1>
<div>
<Type orderType="products" />
</div>
<div style={{ display: "flex", marginTop: 20 }}>
<div style={{ width: "50%" }}>
<Type orderType="options" />
</div>
<div>
<h2>Total Price: {orderDatas.totals.total} </h2>
<br />
<button onClick={() => setStep(1)}>주문하기</button>
</div>
</div>
</div>
);
}
<실제 코드>
<테스트 코드>
But, wrapper로 모든 테스트 케이스마다 감싸주는 것이 비효율적이므로 Custom Render를 만들어서 사용해보자!
So, Custom render를 만들어줘서 그 안에서 wrapper를 처리하고, 일반적인 render()함수처럼 사용하면 되는 것이다!
<test-util.js>
wrapper로 감싸줄 필요가 있는 테스트에는 TRL의 render 말고, Custom Render의 render를 import 하면 된다.
<TRL의 render>
<Custom Render의 render>
👉 우선, 여행 상품과 옵션의 개수에 따라 계산하는 테스트 케이스를 작성
1. getByText를 통해서 '상품 총 가격'이라는 텍스트를 가지고, exact가 false(정확한 값을 모르는)인 앨리맨트를 선택. (가격은 선택에 따른 가변적인 값)
2. 총 가격이 0을 갖는지 유효성 검증한다.(총 가격의 기본값(시작값)은 0부터니까.)
3. findbyRole을 통해 name이 America이고 spinbutton인 앨리먼트를 선택한다.
4. userEvent.clear()을 통해 americaInput에 대한 값을 초기화한다.(만약 그 전에 값이 존재했을 경우)
5. userEvent.type() 함수에는 target.value의 중첩 구조의 이벤트 객체를 넘길 필요가 없이, 실제 입력 텍스트만 넘기면 된다. 따라서, americaInput의 값이 1이 되게 해본다.
6. 그렇다면 productsTotal의 값은 1000이 되는지 유효성 검증한다. (1개 당 1000원)
<calculate.test.js>
참조: userEvent
-https://www.daleseo.com/testing-library-user-agent/
👉 총 total 값의 변화에 따른 테스트 케이스 작성
<Products.js>
<Options.js>