ค้นหาเว็บไซต์

javascript - วิธีการจำลองฟังก์ชันใน Jest


ฉันมีคอมโพเนนต์ง่ายๆ 2 ตัว ฉันกำลังเขียนการทดสอบสำหรับมันโดยใช้ jest ฉันต้องการทดสอบว่าตอนที่ฉันคลิกที่ currencyItem อาร์เรย์ที่เกี่ยวข้องควรถูกเพิ่มเข้าไปในสถานะ select ตามที่คุณเห็น ฉันกำลังส่งฟังก์ชัน handleCurrencyToggle เป็น props ให้กับ currencyItem ดังนั้นฉันจึงต้องการจำลองมันในการทดสอบ jest ด้วย

CurrencySelector.tsx

import { useEffect, useState } from "react";
import { currencies } from "../constants";
import { Currency } from "../types";
import CurrencyItem from "./CurrencyItem";

const CurrencySelector = () => {
  const initialSelected = () => {
    const storedCurrencies = sessionStorage.getItem("currencies");
    return storedCurrencies ? JSON.parse(storedCurrencies) : [];
  };

  const [selected, setSelected] = useState<Currency[]>(initialSelected);

  const handleCurrencyToggle = (currency: Currency) => {
    const isSelected = selected.some(
      (selectedCurrency) => selectedCurrency.name === currency.name
    );

    if (isSelected) {
      setSelected((prevSelected) =>
        prevSelected.filter(
          (selectedCurrency) => selectedCurrency.name !== currency.name
        )
      );
    } else {
      setSelected((prevSelected) => [...prevSelected, currency]);
    }
  };

  useEffect(() => {
    sessionStorage.setItem("currencies", JSON.stringify(selected));
  }, [selected]);

  return (
    <div className="currency-selector">
      <div className="selected-currencies currency-items">
        {selected.length > 0 ? (
          selected.map((currency: Currency, index: number) => (
            <div
              data-testid="selected-currency"
              key={index}
              className="selected-currency currency"
            >
              <span>{currency.name}</span>
              <button
                className="unselect-currency"
                onClick={() => handleCurrencyToggle(currency)}
              >
                x
              </button>
            </div>
          ))
        ) : (
          <div data-testid="no-selected-currencies" className="no-currencies">
            No selected currencies
          </div>
        )}
      </div>
      <div className="currency-items">
        {currencies.map((currency: Currency, index: number) => (
          <CurrencyItem
            data-testid={`currency-item-${index}`}
            key={index}
            currency={currency}
            onClick={() => handleCurrencyToggle(currency)}
            isSelected={selected.some(
              (selectedCurrency) => selectedCurrency.name === currency.name
            )}
          />
        ))}
      </div>
    </div>
  );
};

export default CurrencySelector;

CurrencyItem.tsx

import { Currency } from "../types";

const CurrencyItem = ({
  currency,
  onClick,
  isSelected,
}: {
  currency: Currency;
  onClick: () => void;
  isSelected: boolean;
}) => {
  const handleCheckboxChange = () => {
    onClick();
  };

  return (
    <div data-testid="currency-item" onClick={onClick} className="currency">
      <label>
        <input onChange={handleCheckboxChange} checked={isSelected} type="checkbox" />
        <span className="checkbox-container"></span>
      </label>
      <span>{currency.name}</span>
    </div>
  );
};

export default CurrencyItem;

อาร์เรย์สกุลเงิน:

import { Currency } from "../types";

export const currencies: Currency[] = [
  {
    name: "EUR",
  },
  {
    name: "PLN",
  },
  {
    name: "GEL",
  },
  {
    name: "DKK",
  },
  {
    name: "CZK",
  },
  {
    name: "GBP",
  },
  {
    name: "SEK",
  },
  {
    name: "USD",
  },
  {
    name: "RUB",
  },
];

การทดสอบของฉัน:

  it("adds currency to select list", () => {
    const handleCurrencyToggle = jest.fn()
    const { getByTestId: getByTestId2 } = render(
        <CurrencySelector />
      );
    const { getByTestId } = render(
      <CurrencyItem
        key={1}
        currency={currencies[0]}
        onClick={() => () => {
          handleCurrencyToggle();
        }}
        isSelected={true}
      />
    );
    
    const currencyItem = getByTestId("currency-item");
    fireEvent.click(currencyItem);

    const selectItem = getByTestId2("selected-currency").textContent
    expect(selectItem).toBe("EUR");
  });
});

ฉันรู้ว่ามันไม่ถูกต้อง แต่ขอให้ช่วยแนะนำฉันว่าฉันจะล้อเลียนฟังก์ชัน handleCurrencyToggle ของจริงได้อย่างไร

คำตอบ #1

ในการทดสอบบางสิ่ง คุณต้องกำหนดหน้าที่รับผิดชอบก่อน แยกวัตถุออกจากกันแล้วทดสอบแยกกัน

จากตัวอย่างของคุณ คอมโพเนนต์ CurrencySelector ของคุณมีความรับผิดชอบมากเกินไป มันเข้าถึงการจัดเก็บข้อมูลภายในเครื่อง มันมีความรู้เกี่ยวกับที่อยู่และวิธีการเก็บสกุลเงินที่เลือก คุณจำเป็นต้องเอาส่วนนี้ออกไป

วิธีที่ดีที่สุดในการทำเช่นนั้นคือการมีฮุคที่ปรับแต่งเอง ด้วยวิธีนี้ คุณสามารถจำลองสิ่งต่าง ๆ แยกกันและทดสอบพวกมันแยกจากกัน

คุณอาจต้องใช้ hook useLocalStorage ซึ่งจัดการการใช้งาน local storage ทั้งหมด โค้ดบางส่วนดังนี้:

const useLocalStorage = <T>(key: string, defaultValue?: T) => {
  return {
    setData: (data: T) => localStorage.setItem(key, JSON.stringify(data)),
    getData: () => {
      const data = localStorage.getItem(key);
      if (!data) {
        return defaultValue || undefined;
      }

      return JSON.parse(data) as T;
    },
  };
};

หลังจากนั้นตรรกะทั้งหมดสำหรับการตั้งค่าและการดึงสกุลเงินที่เลือกอาจถูกแยกออกมาเป็นฮุคของตัวเองได้ดังนี้:

import { useCallback, useEffect, useState } from 'react';
import { Currency } from '../Curencies/types';

export const useSelectedCurrency = () => {
  const { setData, getData } = useLocalStorage<Array<Currency>>(
    'currencies',
    []
  );
  const [selected, setSelected] = useState<Array<Currency>>(getData() || []);

  // keeps local storage in sync
  useEffect(() => {
    setData(selected);
  }, [selected]);

  // using useCallback hook to memoize the function reference
  const toggleCurrency = useCallback(
    (currency: Currency) => {
      const isSelected = selected.some(
        (selectedCurrency) => selectedCurrency.name === currency.name
      );

      if (isSelected) {
        setSelected((prevSelected) =>
          prevSelected.filter(
            (selectedCurrency) => selectedCurrency.name !== currency.name
          )
        );
      } else {
        setSelected((prevSelected) => [...prevSelected, currency]);
      }
    },
    [selected]
  );

  return {
    selectedCurrencies: selected,
    toggleCurrency,
  };
};

ตอนนี้ คอมโพเนนต์มีการพึ่งพาเพียงแค่ฮุกที่กำหนดเอง

const CurrencySelector = () => {
  const { selectedCurrencies, toggleCurrency } = useSelectedCurrency();

  return (
    <div className="currency-selector">
      <div className="selected-currencies currency-items">
        {selectedCurrencies.length > 0 ? (
          selectedCurrencies.map((currency: Currency, index: number) => (
            <div
              data-testid="selected-currency"
              key={index}
              className="selected-currency currency"
            >
              <span>{currency.name}</span>
              <button
                className="unselect-currency"
                onClick={() => toggleCurrency(currency)}
              >
                x
              </button>
            </div>
          ))
        ) : (
          <div data-testid="no-selected-currencies" className="no-currencies">
            No selected currencies
          </div>
        )}
      </div>
      <div className="currency-items">
        {selectedCurrencies.map((currency: Currency, index: number) => (
          <CurrencyItem
            data-testid={`currency-item-${index}`}
            key={index}
            currency={currency}
            onClick={() => toggleCurrency(currency)}
            isSelected={selectedCurrencies.some(
              (selectedCurrency) => selectedCurrency.name === currency.name
            )}
          />
        ))}
      </div>
    </div>
  );
};

และเราจำเป็นต้องทดสอบเฉพาะฮุ๊กสำหรับตรรกะการสลับ:

jest.mock('./useLocalStorage', () => ({
  useLocalStorage: jest.fn().mockReturnValue({
    getData: () => [],
    setData: () => {},
  }),
}));

const useLocalStroageMock = useLocalStorage as jest.MockedFunction<
  typeof useLocalStorage
>;

const getCurrencies = (...names: Array<string>): Array<Currency> =>
  names.map((name) => ({ name }));

describe('useSelectedCurrency', () => {
  it('Should return empty array if empt arry if nothig in the local storage', () => {
    const renderResult = renderHook(useSelectedCurrency);

    expect(renderResult.result.current.selectedCurrencies).toEqual([]);
  });

  it(`Should return the items form the local storage as default`, () => {
    const currencies = getCurrencies('USD', 'EUR');
    useLocalStroageMock.mockReturnValue({
      getData: jest.fn().mockReturnValue(currencies),
      setData: jest.fn(),
    });

    const renderResult = renderHook(useSelectedCurrency);

    expect(renderResult.result.current.selectedCurrencies).toEqual(currencies);
  });

  it(`Should remove a currency if a togggle is called iwht name in the selected. t should also remove it from local storage`, () => {
    const currencies = getCurrencies('USD', 'EUR');
    const setDataMock = jest.fn();
    useLocalStroageMock.mockReturnValue({
      getData: jest.fn().mockReturnValue(currencies),
      setData: setDataMock,
    });

    const renderResult = renderHook(useSelectedCurrency);

    act(() => renderResult.result.current.toggleCurrency({ name: 'USD' }));

    expect(renderResult.result.current.selectedCurrencies).toEqual(
      getCurrencies('EUR')
    );
    expect(setDataMock).toHaveBeenCalledWith(getCurrencies('EUR'));
  });
});

ลิงก์ไปยังตัวอย่างที่ใช้งานได้ที่นี่

คำตอบ #2

สุดท้ายฉันก็แก้ปัญหานี้ได้

it("should add currency to the list", () => {
    const handleCurrencyToggle = jest.fn();
    const { getAllByTestId, getByTestId } = render(<CurrencySelector />);
    const { getByTestId: getByTestId2 } = render(
      <CurrencyItem
        index={1}
        key={1}
        currency={currencies[1]}
        onClick={() => () => {
          handleCurrencyToggle({ name: "PLN" });
        }}
        isSelected={true}
      />
    );
    const currencyItems = getAllByTestId("currency-item-1");
    const firstCurrencyItem = currencyItems[0];
    fireEvent.click(firstCurrencyItem);
    const selectItem = getByTestId("selected-currency").textContent;
    expect(selectItem).toBe("PLN");
  });