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