您好,我是单元测试的新手,我有一个组件可以渲染产品列表并显示这些列表。我在该组件中还有一个功能,可以按价格范围过滤这些产品。我使用了 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; Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号
我可以为您提供一些与您的问题相关的建议
检查一个可以帮助您测试 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 和单元测试是两种为您提供最佳服务的方法,具体取决于您要寻找的内容,每种方法都可以模拟用户交互,但其背后的引擎不同
希望对你有帮助