javascript - Hur man mockar funktioner i Jest


Jag har 2 enkla komponenter. Jag skriver tester för dem med jest. Jag vill testa när jag klickar på currencyItem, ska respektive array läggas till i select-tillståndet. Som du kan se skickar jag handleCurrencyToggle-funktionen som props till currencyItem, därför vill jag också mocka den i jest-testet.

ValutaVäljare.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;

valutamatriser:

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",
  },
];

mitt test:

  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");
  });
});

Jag vet att det inte är korrekt, men var snäll och vägled mig hur jag kan imitera den verkliga handleCurrencyToggle-funktionen.

Svar #1

För att testa något behöver du först tilldela ansvar. Separera objekt och testa dem separat.

Baserat på ditt exempel har din CurrencySelector-komponent för många ansvarsområden. Den får åtkomst till lokal lagring, den har kunskap om var och hur de valda valutorna lagras. Du behöver ta bort det.

Det bästa sättet att göra det är att ha några anpassade krokar. På det sättet kan du imitera saker separat och testa dem isolerat.

Du kan behöva useLocalStorage-hooken som hanterar alla manipulationer av lokal lagring. Lite kod här:

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;
    },
  };
};

Efter det kan all logik för att sätta och hämta valda valutor isoleras i sin egen hook på detta sätt:

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,
  };
};

Nu har komponenten beroende ENDAST på den anpassade hooken

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>
  );
};

Och vi behöver bara testa hooken för växlingslogiken:

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'));
  });
});

Länk till ett fungerande exempel här

Svar #2

Jag löste detta till slut.

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");
  });