最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • React Testing library 101 (二)

    正文概述 掘金(an Onion)   2021-03-13   441

    书接上文,上篇说到了 React Testing library 的安装和最基本用法。本篇继续深挖一些较复杂的场景。

    mock 测试

    开始 RTL 测试前,我们稍微回顾一下 Jest 的 Mock 测试。

    其中最常用的 mock 方式有两种:

    1. 创建一个 mock 函数,注入到目标用例里, 如 jest.fn
    2. 设计一个手工的 mock 对象,来覆盖依赖模块,如 jest.mock

    jest.fn()

    先来说 mock 函数注入。我们写一个最最基础的 repeatTen 函数,功能就是调用10 次其他函数。

    function repeatTen(fn) {
      for (let i = 0; i < 10; i++) {
        fn();
      }
    }
    
    repeatTen(() => console.log("Onion"));
    

    repeatTen 函数的特点就是:它根本不关心 fn 的具体实现,只要保证 fn 一次性能跑到10趟就行。这类单元测试就非常适合使用 mock 函数了。如下所示,我们以参数的形式,为 repeatTen 传入一个 mock 函数(onChang),然后检查 mock 函数的调用状态,判定函数是否如期运行就行了。

    it("Count the calls of the onChange", () => {
      const onChange = jest.fn();
      repeatTen(onChange);
      // assert that it is called 10 times
      expect(onChange).toHaveBeenCalledTimes(10);
    });
    

    jest.mock()

    上文 repeatTen 函数的实现比较单纯,现实世界则复杂很多;有些函数的实现会依赖于三方库。在测试环境里你很难以传入一个 mock 函数的形式来模拟真实的运行状态。比如下面这个函数,通过 axios 调用 API 来返回数据,大家想想该怎么写 test case。

    const loadStories = () => axios.get("/stories");
    

    我们要测试 loadStories 但又不大可能调用真实的 API,就只能对 axios 的内部模块动点小脑筋了——用 jest.mock("axios") 自动模拟 axios 模块。

    Jest 提供了一个很有意思的依赖覆盖方法——jest.mock("axios"),它会给对象模块——axios——的.get方法提供一个 mockResolvedValue。通俗来说,就是让axios.get("/stories") 返回一个假的 response;而这个 response 的数据是我们事先准备好的。

    看一下写 mock 依赖测试的主要流程:

    1. 利用 jest.mock(...) 覆盖用例函数内部 import 的依赖
    2. 为用例函数内部使用的某个方法构造一个 mock 返回
    3. 断言用例函数的返回结果
    import axios from "axios";
    
    // step1: override a module
    jest.mock("axios");
    
    const mockStories = [
      { objectID: "1", title: "Hello" },
      { objectID: "2", title: "React" },
    ];
    
    it("load stories by axios", async () => {
      // step2: make a fake response
      const response = { data: mockStories };
      axios.get.mockResolvedValue(response);
    
      // step3: assert the data
      const { data } = await loadStories();
      expect(data).toEqual(mockStories);
    });
    

    callback 测试

    OK,兜兜转转说了很长篇幅的 Jest 方法。我们还是回到 React Testing library(以下简称RTL)。

    如果大家看懂了 Jest.fn 章节的内容,RTL 的 callback 测试就一目了然了——就是来测试 onChange 事件的。我们写一个简单的搜索条,通过 onChange 事件返给父组件输入的内容。

    // CallbackSearch.js
    export function CallbackSearch({ onChange }) {
      return (
        <div>
          <label htmlFor="search">Search:</label>
          <input id="search" type="text" onChange={onChange} />
        </div>
      );
    }
    

    该组件的测试怎么写呢?很简单,反正只有一个 onChange 参数,我们只要测试它的调用状态就行了。

    import userEvent from "@testing-library/user-event";
    
    describe("Search", () => {
      it("paste counts", () => {
        const onChange = jest.fn();
        render(<CallbackSearch onChange={onChange} />);
    
        const $e = screen.getByRole("textbox");
        userEvent.paste($e, "Onion");
    
        expect(onChange).toHaveBeenCalledTimes(1);
      });
    });
    

    这次我们使用的是 userEvent 的paste方法,输入框内黏贴一段文本,自然只触发一次事件,所以断言 onChange 被调用了 1 次即可。假如你用的是userEvent.type,那就是每输入一个字符都会回调一次,就要断言 onChange 的掉用次数为输入字符串的长度了。

    异步加载测试

    上一章讲了 jest.fn 的案例,我们再说说 jest.mock 的案例。

    React Hook

    现在不是流行写 Hook 吗?我们就写个 loadStories 的加强版 Hook,主要功能还是老样子,利用 axios 远程调用 API;成功了就把数据写到 stories 这个 state 里,失败了就把错误写到 error 这个 state 里。

    // userStory.js
    import axios from "axios";
    import { useState, useCallback } from "react";
    
    export function useStory() {
      const [stories, setStories] = useState([]);
      const [error, setError] = useState(null);
    
      const handleFetch = useCallback(() => {
        const loadStories = async () => {
          try {
            const { data } = await axios.get("/stories");
            setStories(data);
          } catch (error) {
            setError(error);
          }
        };
        loadStories();
      }, []);
    
      return { error, stories, handleFetch };
    }
    

    给 React Hook 写单元测试需要额外安装一个依赖,@testing-library/react-hooks;主要是用了它的一个方法 readerHook 来模拟 react 组件的场景。

    我们看一下怎么写这个 Hook 的 test case,套路是相似的:

    1. 利用 jest.mock 覆盖 axios 模块
    2. 伪造一个 axios.get 的 response
    3. 断言 stories 的初试状态是空数组
    4. 调用异步方法加载 stories
    5. 完成异步调用后,断言 stories 的最新状态
    import { renderHook } from "@testing-library/react-hooks";
    import axios from "axios";
    import { useStory } from "./useStory";
    
    // Step 1: override a module
    jest.mock("axios");
    
    const mockStory = [
      { objectID: "1", title: "Hello" },
      { objectID: "2", title: "React" },
    ];
    
    it("load stories and succeed", async () => {
      // Step 2: fake a response
      const response = { data: mockStory };
      axios.get.mockResolvedValue(response);
    
      const { result, waitForNextUpdate } = renderHook(() => useStory());
    
      // Step 3: test initial state
      expect(result.current.stories).toEqual([]);
    
      // Step 4: fetch stories
      result.current.handleFetch();
    
      // Step 5: test after load axios api
      await waitForNextUpdate();
      expect(result.current.stories).toEqual(mockStory);
    });
    

    React Component

    测试完 Hook,我们把它放到组件里。老规矩,先不看实现,看效果:

    React Testing library 101 (二)

    简单来说就是写了一个 button,点击后会异步调用数据,然后展示出所有的 stories 条目。

    还是照搬上面的套路:

    1. 利用 jest.mock 覆盖 axios 模块
    2. 伪造一个 axios.get 的 response
    3. 点击按钮,即异步加载 stories
    4. 结束后,断言屏幕上会出现了相应的 story 条目
    import axios from "axios";
    
    // Step 1: override a module
    jest.mock("axios");
    
    const mockStory = [
      { objectID: "1", title: "Hello" },
      { objectID: "2", title: "React" },
    ];
    
    describe("FetchButton", () => {
      it("fetches stories from an API and displays them", async () => {
        render(<FetchButton />);
    
        // Step 2: fake a response
        const response = { data: mockStory };
        axios.get.mockResolvedValue(response);
    
        // Step 3: click button
        userEvent.click(screen.getByRole("button"));
    
        // Step 4: assert that screen will display 2 items
        const $items = await screen.findAllByRole("listitem");
        expect($items).toHaveLength(2);
      });
    });
    

    教课书里的 TDD 要求先写测试,再写实现的。我这里也尽量按照这个思路排版,大家看完单元测试,也应该有了大体的组件实现轮廓了吧?我把我的实现写出来:

    // FetchButton.js
    import { useStory } from "./useStory";
    
    export function FetchButton() {
      const { error, stories, handleFetch } = useStory();
    
      return (
        <div>
          <button onClick={handleFetch}>Fetch Stories</button>
    
          {error && <span>Something went wrong ...</span>}
    
          <ul>
            {stories.map((story) => (
              <li key={story.objectID}>{story.title}</li>
            ))}
          </ul>
        </div>
      );
    }
    

    异常测试

    我们接着说上面的 FetchButton。在写实现的时候,我发现了之前测试里有个重大疏漏:异步调取的 API 可能会返回错误,之前的单元测试没有覆盖到抛异常的情况。所以我又在实现里加了个 error 的判断;有错误就显示 Something went wrong ...。既然有疏漏,就继续补 test case。这种测试其实更简单,基本上就是套用上文的步骤,唯一不同之处就是让 axios.get 返回mockRejectedValue。大家看看下面的 test case,应该已经没有难点了。

    jest.mock("axios");
    
    describe("FetchButton", () => {
    
      it("fetch stories from an API but fail", async () => {
    
        render(<FetchButton />);
    
    +    axios.get.mockRejectedValue(new Error()));
    
        userEvent.click(screen.getByRole("button"));
    
        const $errorMsg = await screen.findByText(/Something went wrong/);
    
        expect($errorMsg).toBeInTheDocument();
      });
    });
    

    小结

    我最近又回看了2020 年 JS 满意度调查,发现 Testing Library 位居测试榜榜首,还是很有群众基础的。我们用了两期时间介绍了 Testing Library 的入门教程,也希望大家能尽快把 TDD 落实到自己的项目中。我见过好多项目几乎没有任何测试,一两年就烂掉了;一加新功能就四处漏风,后期 bug 数量急速上升,release 甚至能因此拖延半年之久;屎山一堆积,想改就再也改不了了。TDD 是软件行业多年来的最佳实践,我们作为该行业的从业人员,也应该坚定地按行业规律办事;这不仅是“干活勤快”这么简单,更多的是自己职业素养的体现,共勉。

    React Testing library 101 (二)

    相关

    • 《React Testing library 101 (一)》

    起源地下载网 » React Testing library 101 (二)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元