안녕하세요, 저는 단위 테스트를 처음 접했습니다. 제품 목록을 렌더링하고 해당 목록을 표시하는 구성 요소가 있습니다. 또한 이 구성요소에는 이러한 제품을 가격대별로 필터링하는 기능도 있습니다. 나는 기본적으로 두 개의 상태 변수 "priceFrom"과 "priceTo"를 업데이트하는 ReactSlider를 사용하고 있으며 이러한 상태 변수가 변경되면 해당 가격을 기준으로 제품을 필터링하는 함수를 트리거합니다. 그래서 비슷한 기능을 농담으로 테스트해보고 싶었습니다. 농담이 가능한가요? 그렇다면 이를 어떻게 달성할 수 있으며, 그렇지 않은 경우 이 구성 요소의 단위 테스트를 수행하기 위해 어떤 대안을 취할 수 있습니까? 감사해요. 이 구성 요소의 코드는 다음과 같습니다. 아래 코드에서 ReactSlider가 값을 업데이트하면 filterItem 함수가 제품을 필터링하고 UI를 업데이트합니다.
import React, { useEffect, useState, useContext } from "react"; import { useParams } from "react-router-dom"; import styles from './productsList.module.css'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faSpinner } from '@fortawesome/free-solid-svg-icons'; import Backdrop from "../../Others/Backdrop/backdrop"; import Modal from '../../Others/Modal/modal'; import ReactPaginate from 'react-paginate'; import ReactSlider from 'react-slider'; import { disableScroll } from "../../Others/HelperFunction/helperFunction"; import { ContextProvider } from "../../Others/AuthContext/authContext"; import './slider.css'; const ProductsList = () => { const context = useContext(ContextProvider); const productId = useParams().productId; const [sidebar, setSidebar] = useState(false); const [backdrop, setBackdrop] = useState(false); const [products, setProducts] = useState([]); const [filteredProducts, setFilteredProducts] = useState([]); const [error, setError] = useState(false); const [status, setStatus] = useState(''); const [modal, setModal] = useState(false); const [priceFrom, setPriceFrom] = useState(0); const [priceTo, setPriceTo] = useState(10000); const [itemOffset, setItemOffset] = useState(0); const [itemNotFound, setItemNotFound] = useState(false); const itemPerPage = 9; const endOffset = itemOffset + itemPerPage; let pageCount = 0; if (filteredProducts.length){ pageCount = Math.ceil(filteredProducts.length / itemPerPage); } else { pageCount = Math.ceil(products.length / itemPerPage); } const handlePageClick = (event) => { if (filteredProducts.length){ const newOffset = (event.selected * itemPerPage) % filteredProducts.length; setItemOffset(newOffset); } else { const newOffset = (event.selected * itemPerPage) % products.length; setItemOffset(newOffset); } } useEffect(() => { window.scrollTo(0, 0); if (context.data !== undefined){ const product = context.data[productId] !== undefined ? context.data[productId] : []; if (product.length){ setProducts(product); setStatus('success'); } else { setStatus('not found'); } } }, [context.data] ); useEffect(() => { window.scrollTo(0, 0); }, [itemOffset, filteredProducts.length]); useEffect(() => { if (backdrop){ disableScroll(); } else { window.onscroll = () => { } } }, [backdrop]) let defaultView = Array.from(Array(12).keys()).map(item => { return <div key={item} className={styles.defaultItemContainer} id={styles.loader}> <div className={styles.defaultItemImgContainer}> <FontAwesomeIcon icon={faSpinner} spinPulse className={styles.spinnerPulse} /> </div> <div className={styles.loadingName}></div> <div className={styles.loadingLink}></div> </div> }); if (products.length){ if (!itemNotFound && filteredProducts.length){ defaultView = filteredProducts.slice(itemOffset, endOffset).map(item => { return <div key={item._id} className={styles.productsContainer} id={styles.loader}> <a href={`/products/${productId}/${item.name}`} className={styles.productsLink}> <div className={styles.productsImgContainer}> <img src={item.img[0]} alt={item.name} className={styles.productsImg}/> </div> <div className={styles.productsName}>{item.name}</div> <div className={styles.productsPrice}>৳{item.price}</div> </a> </div> }); } else if (itemNotFound) { defaultView = <div className={styles.notFoundContainer}> <h2 className={styles.notFoundHeader}>Nothing found based on your range</h2> </div> } else { defaultView = products.slice(itemOffset, endOffset).map(item => { return <div key={item._id} className={styles.productsContainer} id={styles.loader}> <a href={`/products/${productId}/${item.name}`} className={styles.productsLink}> <div className={styles.productsImgContainer}> <img src={item.img[0]} alt={item.name} className={styles.productsImg}/> </div> <div className={styles.productsName}>{item.name}</div> <div className={styles.productsPrice}>৳{item.price}</div> </a> </div> }); } } else if(status === 'not found') { defaultView = <div className={styles.fallbackContainer}> <h2 className={styles.fallbackHeader}>Nothing found</h2> </div> } const filterItem = () => { if (priceFrom && priceTo) { const filteredData = products.filter(item => Number(item.price) >= priceFrom && Number(item.price) <= priceTo); if (filteredData.length){ setItemNotFound(false) setFilteredProducts(filteredData); setSidebar(false); setBackdrop(false); } else { setItemNotFound(true); setItemOffset(0); setSidebar(false); setBackdrop(false); } } } const resetFilter = () => { setPriceFrom(0); setPriceTo(1000); setFilteredProducts([]); } const openSidebar = () => { if (!sidebar){ setSidebar(true); setBackdrop(true); } } const closeSidebar = () => { if (sidebar){ setSidebar(false); setBackdrop(false); } else { setBackdrop(false); } } let displayStatus = <div className={styles.statusMsgContainer}> <h2 className={styles.statusMsgHeader}>Something went wrong</h2> <p className={styles.statusMsgP}>Please try again</p> <button className={styles.statusMsgBtn} onClick={() => { setModal(false); }}>Ok</button> </div> if (status === 'database error'){ displayStatus = <div className={styles.statusMsgContainer}> <h2 className={styles.statusMsgHeader}>Database error</h2> <p className={styles.statusMsgP}>Please try again or contact the admin</p> <button className={styles.statusMsgBtn} onClick={() => { setError(false); setStatus(''); setModal(false); }}>Ok</button> </div> } return ( <> <Backdrop backdrop={ backdrop } toggleBackdrop={ closeSidebar }/> <Modal modal={modal}> {displayStatus} </Modal> <div className={styles.productsListMain}> <div className={styles.productsListContainer}> <div className={styles.sidebarSwitcher} onClick={ openSidebar }> <p className={styles.sidebarSwitcherP}>Show Sidebar</p> </div> <div className={ sidebar ? `${styles.sidebarContainer} ${styles.on}` : styles.sidebarContainer}> <div className={styles.categoryType}> <h2 className={styles.categoryH2}>Categories</h2> <ul className={styles.sidebarLists}> <a href="/products/Bracelet" className={styles.sidebarLink}><li className={productId === 'Bracelet' ? `${styles.sidebarList} ${styles.active}` : styles.sidebarList}>Bracelets</li></a> <a href="/products/Finger Ring" className={styles.sidebarLink}><li className={productId === 'Finger Ring' ? `${styles.sidebarList} ${styles.active}` : styles.sidebarList}>Finger Rings</li></a> <a href="/products/Ear Ring" className={styles.sidebarLink}><li className={productId === 'Ear Ring' ? `${styles.sidebarList} ${styles.active}` : styles.sidebarList}>Ear Rings</li></a> <a href="/products/Necklace" className={styles.sidebarLink}><li className={productId === 'Necklace' ? `${styles.sidebarList} ${styles.active}` : styles.sidebarList}>Necklace</li></a> <a href="/products/Toe Ring" className={styles.sidebarLink}><li className={productId === 'Toe Ring' ? `${styles.sidebarList} ${styles.active}` : styles.sidebarList}>Toe Ring</li></a> <a href="/products/Other" className={styles.sidebarLink}><li className={productId === 'Other' ? `${styles.sidebarList} ${styles.active}` : styles.sidebarList}>Others</li></a> </ul> </div> <div className={styles.categoryType} id={styles.categoryType2}> <h2 className={styles.categoryH2}>Price Range</h2> <ReactSlider max={10000} className="horizontal-slider" thumbClassName="example-thumb" trackClassName="example-track" value={[priceFrom, priceTo]} ariaLabel={['Lower thumb', 'Upper thumb']} ariaValuetext={state => `Thumb value ${state.valueNow}`} renderThumb={(props, state) => <div {...props}>{state.valueNow}</div>} minDistance={1} onChange={([v1, v2]) => { setPriceFrom(v1); setPriceTo(v2); }} pearling /> <button disabled={!priceFrom || !priceTo} className={styles.filterBtn} onClick={ resetFilter }>Reset</button> <button disabled={(!priceFrom || !priceTo) || (priceFrom >= priceTo)} className={styles.filterBtn} onClick={filterItem}>Apply</button> </div> </div> <div className={styles.ProductsLists}> <h2 className={styles.productHeader}>{products.length ? products[0].category : null}</h2> <div className={styles.productsDisplayContainer}> {defaultView} </div> </div> </div> <ReactPaginate breakLabel="..." nextLabel=">" className={styles.paginationContainer} pageClassName={styles.paginationItem} previousClassName={styles.previousItem} nextClassName={styles.nextItem} activeClassName={styles.paginationActive} disabledClassName={styles.paginationDisabled} onPageChange={handlePageClick} pageRangeDisplayed={5} pageCount={itemNotFound ? 0 : pageCount} previousLabel="<" renderOnZeroPageCount={null}/> </div> </> ) } export default ProductsList;
귀하의 문제와 관련된 몇 가지 제안을 제공해드릴 수 있습니다
React 요소를 테스트하는 데 도움이 되는 라이브러리를 확인하세요(예: React-testing-library https://testing-library.com/docs/react-testing-library/intro/)
일반적으로 다른 라이브러리의 내용은 라이브러리 자체에서 테스트했다고 가정하기 때문에 테스트되지 않습니다(이 경우 ReactSlider 라이브러리
구성 요소를 더 작은 구성 요소로 분할해 보세요. 테스트하기가 더 쉽고(props를 사용할 수 있음) 각 구성 요소 내부의 종속성이 줄어듭니다
여기서 테스트를 두 가지 테스트 E2E와 단위 테스트로 나눌 수 있습니다. E2E는 사용자 상호 작용을 시뮬레이션하는 도구를 사용하는 경우입니다. 이 경우 범위 슬라이더를 움직이는 사용자를 시뮬레이션합니다. 그 후에 무슨 일이 일어나는지 확인하세요. (이를 위해 Cypress 또는 Selenium을 사용할 수 있습니다.) 이러한 도구는 브라우저에서 사람처럼 실행되며, 단위 테스트를 통해 기능, 구성 요소 입력 및 출력을 테스트할 수 있습니다. 여기에서는 클릭 및 상호 작용도 시뮬레이션할 수 있습니다. 그러나 사용자 상호 작용의 경우에는 그렇지 않습니다. 단위 테스트(이 경우 농담 도구)는 실제 브라우저의 시뮬레이션인 JSDOM에서 테스트를 실행하기 때문에 E2E만큼 강력합니다(간단히 말하면)
이 문제를 해결하기 위해 다음을 수행하겠습니다
사이프러스: https://www.cypress.io/
셀레늄: https://www.selenium.dev/
E2E와 단위 테스트는 찾고 있는 항목에 따라 가장 적합한 두 가지 접근 방식이라는 점을 명심하세요. 각 접근 방식은 사용자 상호 작용을 시뮬레이션하지만 그 뒤에는 다른 엔진이 있습니다.
도움이 되길 바랍니다