首頁  >  問答  >  主體

如何在玩笑中測試組件功能

您好,我是單元測試的新手,我有一個元件可以渲染產品清單並顯示這些清單。我在該組件中還有一個功能,可以按價格範圍過濾這些產品。我使用了 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粉949848849430 天前651

全部回覆(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
  • 取消回覆