搜索

首页  >  问答  >  正文

如何在玩笑中测试组件功能

您好,我是单元测试的新手,我有一个组件可以渲染产品列表并显示这些列表。我在该组件中还有一个功能,可以按价格范围过滤这些产品。我使用了 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}>&#2547;{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}>&#2547;{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;

P粉949848849P粉949848849445 天前673

全部回复(1)我来回复

  • P粉794851975

    P粉7948519752023-09-08 22:06:24

    我可以为您提供一些与您的问题相关的建议

    • 检查一个可以帮助您测试 React 元素的库(例如react-testing-library https://testing-library.com/docs/react-testing-library/intro/)

    • 通常来自另一个库的东西不会被测试,因为我们假设库本身已经测试过它(在这种情况下是 ReactSlider 库

    • 尝试将组件拆分为更小的组件,这样更容易测试(您可以使用 props),并且每个组件内部的依赖关系会更少

    • 我们可以将这里的测试分为两种不同的测试E2E单元测试,E2E是当您使用模拟用户交互的工具时,在这种情况下您可以模拟用户移动范围滑块,看看之后会发生什么(为此,您可以使用 cypress 或 selenium)这些工具像人类一样在浏览器中运行,单元测试允许您测试功能、组件输入和输出,在这里您可以也模拟点击和交互,但对于用户交互来说,它们不如 E2E 强大,因为单元测试(在本例中是开玩笑的工具)在 JSDOM 中运行测试,JSDOM 是真实浏览器的模拟(简单来说)

    为了解决这个问题,我将执行以下操作

    • 安装并查看测试库和 jest
    • 将组件拆分为更小的组件
    • 尝试跳过 ReactSlider 测试本身并将其委托给库
    • 通过单元测试,您可以测试向其传递 props 的过滤器(将其分离到另一个组件中之后),并使用测试库检查传递给它的不同 props 时它的行为方式
    • 使用 Cypress 或 Selenium,您可以更深入地测试在浏览器中移动滑块时会发生什么

    赛普拉斯:https://www.cypress.io/
    硒: https://www.selenium.dev/

    请记住,E2E 和单元测试是两种为您提供最佳服务的方法,具体取决于您要寻找的内容,每种方法都可以模拟用户交互,但其背后的引擎不同

    希望对你有帮助

    回复
    0
  • 取消回复