Issue
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.
Solution
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
Answered By - Svetoslav Petkov
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.