skip to Main Content

I have 2 simple components. I am writing tests for them using jest. I want to test when I click on currencyItem, the respective array should be added to the select state. As you can see I am passing handleCurrencyToggle function as props to the currencyItem hence I want to mock it in jest test too.

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;

currency array:

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

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

I know it’s not correct but please guide me how can I mock the real handleCurrencyToggle function.

3

Answers


  1. Chosen as BEST ANSWER

    I eventually solved this.

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

  2. To properly test the integration between CurrencySelector and CurrencyItem components, you can mock the handleCurrencyToggle function in the CurrencySelector component and ensure that it is called with the correct parameters when a CurrencyItem is clicked.

    import CurrencySelector from "./CurrencySelector";
    import { currencies } from "../constants";
    
    describe("CurrencySelector", () => {
      it("adds currency to select list", () => {
        const mockedHandleCurrencyToggle = jest.fn();
    
        // Mock the handleCurrencyToggle function
        jest.mock("../CurrencySelector", () => ({
          __esModule: true,
          default: () => <div data-testid="currency-selector" />,
          handleCurrencyToggle: mockedHandleCurrencyToggle,
        }));
    
        const { getByTestId } = render(
          <CurrencyItem
            key={1}
            currency={currencies[0]}
            onClick={() => mockedHandleCurrencyToggle()}
            isSelected={true}
          />
        );
    
        const currencyItem = getByTestId("currency-item");
        fireEvent.click(currencyItem);
    
        // Ensure that handleCurrencyToggle is called with the correct parameters
        expect(mockedHandleCurrencyToggle).toHaveBeenCalledWith(currencies[0]);
      });
    });                                                                                                                                                             
    

    the handleCurrencyToggle function is mocked and replaced with mockedHandleCurrencyToggle in the CurrencySelector component. Then, in your test for CurrencyItem, you can check if mockedHandleCurrencyToggle is called with the expected parameters.

    The above code assumes that the CurrencySelector component imports handleCurrencyToggle from the same file. If it’s imported from a different file, you should adjust the jest.mock path accordingly.

    Login or Signup to reply.
  3. To test something, you need first to assign responsibilities. Separate objects apart and test them separately.

    Based on your example, your CurrencySelector component has too many responsibilities. It accesses the local storage, it has the knowledge where and how the selected currencies are stored. You need to take that away.

    The best way to do that is to have some custom hooks. This way, you can mock things separately and test them in isolation.

    You may need useLocalStorage hook which handles all local storage manipulations.
    Some code here:

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

    After that all logic for setting and getting selected currencies may be isolate into its own hook like this:

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

    Now, the component has dependency ONLY on the custom hook

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

    And we need to test only the hook for the toggling logic:

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

    Link to a working example here

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search