在我头脑混乱之后,我想通了。
在 Jest 中,每当您使用外部源(例如使用 fetch 或 axios 的 API 调用)时,您都必须模拟它。这意味着 Jest 将接受来自您的组件或函数的任何 axios 请求,而不是调用真正的 axios,它会自动调用您的模拟 axios。这是我缺少的解释,也是我困惑的根源。 jest mocking 的美妙之处在于,您将始终为您的测试获得相同的数据,从而保持结果和断言的一致性。
有很多方法可以使用 Jest 模拟 Axios,包括用于此特定目的的库,例如 jest-mock-axios 和 MSW(模拟服务工作者),但我无法让它们在我的情况下工作。
我发现了一种更简单的方法,不需要以下 YouTube 教程中描述的外部库。这家伙知道如何解释事情,他有一个使用 MSW 的更新视频(YouTube cmets 中的链接)。
YouTube: Mocking Axios in Jest + Testing Async Functions
解决方案
这是要测试的组件,可以看到mount时有useEffect触发的axios请求。
/screens/Home.tsx
import React, { useState, useEffect, memo } from "react";
import { FlatList, StyleSheet } from "react-native";
import { Button } from "react-native-elements";
import axios from "axios";
import Item from "../components/Item";
import AppConfig from "../AppConfig.json";
import { View, Text, ActivityIndicator } from "../components/Themed";
import Toast from "react-native-toast-message";
import { RootTabScreenProps } from "../types";
let _isMounted = false;
function Home({ navigation }: RootTabScreenProps<"Shop">) {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState("");
useEffect(() => {
_isMounted = true;
loadData();
return () => {
_isMounted = false;
};
}, []);
async function loadData(cb?: any) {
try {
axios.get(`${AppConfig.api}/products`).then((res) => {
if (!_isMounted) return;
if (res.status === 200) {
const { data } = res;
setItems(data);
} else {
setError(`Error ${res.status}: failed to load products`);
}
setLoading(false);
setRefreshing(false);
if (typeof cb === "function") cb();
});
} catch (error) {
setLoading(false);
setRefreshing(false);
setError("Failed to load products");
// console.log(error);
}
}
if (loading && !error) {
return (
<View style={styles.containerCenter}>
<ActivityIndicator size={"large"} color="primary" />
</View>
);
} else if (!loading && error) {
return (
<View style={styles.containerCenter}>
<Text>{error}</Text>
<Button
title="Try again"
onPress={() => {
setLoading(true);
setError("");
loadData();
}}
/>
</View>
);
} else {
return (
<View style={styles.container}>
<FlatList
columnWrapperStyle={{ justifyContent: "space-between" }}
data={items}
numColumns={2}
renderItem={({ item }: any) => {
return (
<Item
item={item}
onPress={() => navigation.push("Product", item)}
/>
);
}}
keyExtractor={(item: object, index: any) => index}
refreshing={refreshing}
onRefresh={() => {
setRefreshing(true);
loadData(() => {
Toast.show({
type: "success",
text1: "Product list refreshed",
position: "bottom",
});
});
}}
/>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
containerCenter: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
title: {
fontSize: 20,
fontWeight: "bold",
},
separator: {
marginVertical: 30,
height: 1,
width: "80%",
},
textError: {
fontSize: 18,
marginBottom: 10,
maxWidth: 250,
},
});
export default memo(Home);
第 1 步
在项目的根目录下创建一个名为 __mocks__ 的文件夹(或您的源代码所在的任何位置!)并创建一个名为 axios.js 的文件,其中包含以下对象:
/__mocks__/axios.js
export default {
get: jest.fn(() =>
Promise.resolve({
headers: {},
config: {},
status: 200,
statusText: "OK",
data: [
{
id: 1,
title: "Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops",
price: 109.95,
description:
"Your perfect pack for everyday use and walks in the forest. Stash your laptop (up to 15 inches) in the padded sleeve, your everyday",
category: "men's clothing",
image: "https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg",
rating: { rate: 3.9, count: 120 },
},
{
id: 2,
title: "Mens Casual Premium Slim Fit T-Shirts ",
price: 22.3,
description:
"Slim-fitting style, contrast raglan long sleeve, three-button henley placket, light weight & soft fabric for breathable and comfortable wearing. And Solid stitched shirts with round neck made for durability and a great fit for casual fashion wear and diehard baseball fans. The Henley style round neckline includes a three-button placket.",
category: "men's clothing",
image:
"https://fakestoreapi.com/img/71-3HjGNDUL._AC_SY879._SX._UX._SY._UY_.jpg",
rating: { rate: 4.1, count: 259 },
},
{
id: 3,
title: "Mens Cotton Jacket",
price: 55.99,
description:
"great outerwear jackets for Spring/Autumn/Winter, suitable for many occasions, such as working, hiking, camping, mountain/rock climbing, cycling, traveling or other outdoors. Good gift choice for you or your family member. A warm hearted love to Father, husband or son in this thanksgiving or Christmas Day.",
category: "men's clothing",
image: "https://fakestoreapi.com/img/71li-ujtlUL._AC_UX679_.jpg",
rating: { rate: 4.7, count: 500 },
},
],
})
),
};
将来自您的 Promise.resolve(...) 的响应调整为您期望您的真实 API 返回的任何内容。另外,请确保您使用 jest.fn() 模拟该函数,否则这将不起作用。
您还可以为您模拟的 axios 对象添加不同的属性,例如发布、更新、放置或您需要模拟的任何类型的请求。
第 2 步
再次在源代码的根目录中,创建一个名为 __tests__ 的文件夹,并在其中创建一个名为 screens 的文件夹。然后创建您的测试文件,以保持一致,我将其命名为 Home.test.js。
/__tests__/screens/Home.test.js
import renderer from "react-test-renderer";
import axios from "axios";
import { act } from "@testing-library/react-native";
import Home from "../../screens/Home";
// Important:
// By calling this, jest will know not to use the real axios and will load it
// from your __mocks__ folder.
jest.mock("axios");
describe("<Home />", () => {
let wrapper;
it("renders items", async () => {
await act(async () => {
// This is where the magic happens, when you render your Home component and useEffect
// goes to perform your axios request jest will automatically call your __mocks__/axios instead
wrapper = await renderer.create(<Home />);
});
await expect(wrapper.toJSON()).toMatchSnapshot();
});
it("renders error", async () => {
await act(async () => {
// You can also override your __mocks__/axios by doing the following and simulate a different
// response from your mocking axios
await axios.get.mockImplementationOnce(() =>
Promise.resolve({
status: 400,
statusText: "400",
headers: {},
config: {},
})
);
wrapper = await renderer.create(<Home />);
});
await expect(wrapper.toJSON()).toMatchSnapshot();
});
});
现在,当您运行 npm run test 时,您的 Home 组件将从您的 __mocks__/axios 接收数据并按预期呈现它,您可以对其执行各种测试。
这真的很酷!