【问题标题】:React Native: How to test a async component snapshot with Jest/Testing-library?React Native:如何使用 Jest/Testing-library 测试异步组件快照?
【发布时间】:2022-02-18 01:46:43
【问题描述】:

我正在尝试为 React Native 中的 async 组件创建一个测试。该组件使用useEffect 获取数据,将其设置为状态变量并相应地加载屏幕。全部加载后,我想将其与快照进行比较。我遇到的问题是我的测试是同步的,当我检查渲染的快照时,它有我的加载指示器。

如何等待它加载数据然后执行测试?

我找到的所有示例和教程都是针对同步组件的,涉及简单的任务,例如检查特定标题的按钮,这个和另一个。我已经尝试过waitFor 函数,但它在获取数据之前超时,显然它有 5 秒的限制。或者也许我应该模拟一个 fetch (?) 但我的组件不需要任何道具来将数据注入其中。

说实话,我对如何处理这个问题感到非常困惑。我以前从未做过任何自动化测试。

【问题讨论】:

  • 显示最小代码

标签: react-native jestjs react-testing-library


【解决方案1】:

在我头脑混乱之后,我想通了。

在 Jest 中,每当您使用外部源(例如使用 fetch 或 axios 的 API 调用)时,您都必须模拟它。这意味着 Jest 将接受来自您的组件或函数的任何 axios 请求,而不是调用真正的 axios,它会自动调用您的模拟 axios。这是我缺少的解释,也是我困惑的根源。 jest mocking 的美妙之处在于,您将始终为您的测试获得相同的数据,从而保持结果和断言的一致性。

有很多方法可以使用 Jest 模拟 Axios,包括用于此特定目的的库,例如 jest-mock-axiosMSW(模拟服务工作者),但我无法让它们在我的情况下工作。

我发现了一种更简单的方法,不需要以下 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 接收数据并按预期呈现它,您可以对其执行各种测试。

这真的很酷!

【讨论】:

    猜你喜欢
    • 2020-01-20
    • 2021-05-27
    • 2021-02-11
    • 2019-10-30
    • 2020-03-22
    • 2021-02-28
    • 1970-01-01
    • 2023-04-04
    • 1970-01-01
    相关资源
    最近更新 更多