您好,我是單元測試的新手,我有一個元件可以渲染產品清單並顯示這些清單。我在該組件中還有一個功能,可以按價格範圍過濾這些產品。我使用了 ReactSlider,它基本上更新了兩個狀態變數“priceFrom”和“priceTo”,當這些狀態變數發生變化時,我觸發了一個根據這些價格過濾產品的函數。所以我想在玩笑中測試類似的功能。開玩笑有可能嗎?如果是這樣,我怎麼能實現這一點,如果不是,我可以採取哪種替代方法來完成該組件的單元測試。謝謝。該組件的程式碼如下。請注意,在下面的程式碼中,當 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)這些工具像人類一樣在瀏覽器中運行,單元測試允許您測試功能、元件輸入和輸出,在這裡您可以也模擬點擊和交互,但對於用戶交互來說,它們不如E2E 強大,因為單元測試(在本例中是開玩笑的工具)在JSDOM 中運行測試,JSDOM 是真實瀏覽器的模擬(簡單來說)
為了解決這個問題,我將執行以下操作
賽普拉斯:https://www.cypress.io/
硒: https://www.selenium.dev/
#請記住,E2E 和單元測試是兩種為您提供最佳服務的方法,具體取決於您要尋找的內容,每種方法都可以模擬用戶交互,但背後的引擎不同
希望對你有幫助