>  Q&A  >  본문

Jest에서 구성요소 기능을 테스트하는 방법

안녕하세요, 저는 단위 테스트를 처음 접했습니다. 제품 목록을 렌더링하고 해당 목록을 표시하는 구성 요소가 있습니다. 또한 이 구성요소에는 이러한 제품을 가격대별로 필터링하는 기능도 있습니다. 나는 기본적으로 두 개의 상태 변수 "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}>&#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粉949848849379일 전578

모든 응답(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을 사용할 수 있습니다.) 이러한 도구는 브라우저에서 사람처럼 실행되며, 단위 테스트를 통해 기능, 구성 요소 입력 및 출력을 테스트할 수 있습니다. 여기에서는 클릭 및 상호 작용도 시뮬레이션할 수 있습니다. 그러나 사용자 상호 작용의 경우에는 그렇지 않습니다. 단위 테스트(이 경우 농담 도구)는 실제 브라우저의 시뮬레이션인 JSDOM에서 테스트를 실행하기 때문에 E2E만큼 강력합니다(간단히 말하면)

    이 문제를 해결하기 위해 다음을 수행하겠습니다

    • 테스트 라이브러리와 jest를 설치하고 확인하세요
    • 구성 요소를 더 작은 구성 요소로 분할
    • ReactSlider 테스트 자체를 건너뛰고 라이브러리에 위임해보세요
    • 단위 테스트를 사용하면 props를 전달하는 필터를 테스트할 수 있으며(다른 구성 요소로 분리한 후) 테스트 라이브러리를 사용하여 다른 props가 전달될 때 필터가 어떻게 작동하는지 확인할 수 있습니다
    • Cypress 또는 Selenium을 사용하면 브라우저에서 슬라이더를 움직일 때 어떤 일이 발생하는지 더 자세히 테스트할 수 있습니다

    사이프러스: https://www.cypress.io/
    셀레늄: https://www.selenium.dev/

    E2E와 단위 테스트는 찾고 있는 항목에 따라 가장 적합한 두 가지 접근 방식이라는 점을 명심하세요. 각 접근 방식은 사용자 상호 작용을 시뮬레이션하지만 그 뒤에는 다른 엔진이 있습니다.

    도움이 되길 바랍니다

    회신하다
    0
  • 취소회신하다