【学习】前端测试 jest (三)

352 阅读5分钟

上一篇:【学习】前端测试 jest (二)

【补充】 发现一篇不错的教程,专门针对 react-native 的:携程租车React Native单元测试实践

这次是 jest 学习的第三次,这次主要是分组相关的,也就是 describe afterAll afterEach beforeAll beforeEach 这些的用法。

我看到一句话,如果你写的代码是不可测试的,那么你的代码就是不好的,那么是不是可以进行扩展,比方说:不能实现的产品设计就不是好产品。如果一个东西是可测试的,但被你设计成不可测试的,那就是糟糕的,假如说你们公司把写单元测试的和写程序的分开,如果你写的代码交给写单位测试的同事,他会怎么说你呢。

1. 使用 jest 测试类组件

我想直接使用 jest 提供的直接测试,像下面这样:

class TestForm extends React.PureComponent {
  initData = (data) => {
    const {name, sex, age} = data;
    return {name, sex, age};
  };

  state = this.initData(this.props.data);

  onChangeName = (name) => {
    this.setState({name});
  };
  onChangeSex = (sex) => {
    this.setState({sex});
  };
  onChangeAge = (age) => {
    const tempAge = parseInt(age, 10);
    this.setState({age: Number.isNaN(tempAge) ? 0 : tempAge});
  };

  render() {
    const {name, sex, age} = this.state;
    return (
      <View>
        <TextInput
          placeholder={'姓名'}
          onChangeText={this.onChangeName}
          value={name}
        />
        <TextInput
          placeholder={'性别'}
          onChangeText={this.onChangeSex}
          value={sex}
        />
        <TextInput
          placeholder={'年龄'}
          onChangeText={this.onChangeAge}
          value={age?.toString()}
        />
      </View>
    );
  }
}

原本我测试一下 onChangeName 这个方法,我想通过示例化对象来进行:

const form = new TestForm({data: {name: '吴敬悦', age: 27, sex: '男'}});
it('测试 form 组件里面的 onChangeName 方法', () => {
  form.onChangeName('吴天歌');
  expect(form.state.name).toBe('吴天歌');
});

发现并不能通过,提示 实际值是“吴敬悦”,而不是吴天歌。 为啥会出现这样的情况呢,就像上面说的那样, Can't call setState on a component that is not yet mounted. This is a no-op, but it might indicate a bug in your application. Instead, assign to 'this.state' directly or define a 'state = {};' class property with the desired state in the TestForm component.,所以这样子是不行的,这个时候就不得不改变策略了。

2. 学习 enzyme

我先在 react 中进行初步的学习,然后再到 react-native 中使用。

2.1 在 react 中使用 enzyme

准备工作, 安装这个的时候注意一下你的 react 版本,我的是最新的,所以我也使用最新的:

npm i --save-dev enzyme enzyme-adapter-react-16

首先写一个简单的示例

import { useCallback, useEffect, useState } from "react";
import "./App.css";

function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setCount(10);
  }, []);

  const add = useCallback(() => {
    setCount((count) => count + 1);
  }, []);
  const sub = useCallback(() => {
    setCount((count) => count - 1);
  }, []);
  return (
    <div className="App">
      <h1>{count}</h1>
      <button className={"button"} onClick={add}>
        +
      </button>
      <button className={"button"} onClick={sub}>
        -
      </button>
    </div>
  );
}

export default App;

测试:

import App from "./App";
import Enzyme, { shallow } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
// 这些是配置适配器,必须要添加,当然也可以配置到 jest 中,由于我只是单个文件,所以就先这样了
Enzyme.configure({ adapter: new Adapter() });

it("这是测试 App 这个组件 里面的 button 组件的个数", () => {
  const wrapper = shallow(<App />);
  expect(wrapper.find("button").length).toBe(2);
});

it("测试 App 这个组件的里面的第一个 button 组件的 onClick 事件", () => {
  const wrapper = shallow(<App />);
  const firstButton = wrapper.find("button").first();
  firstButton.props().onClick();
  expect(wrapper.find("h1").first().props().children).toBe(1);
});

先看看 shallow ,这个是浅渲染,也就是只渲染当前的组件,不会渲染下面的子节点。

可以通过 wrapper.find 找到当前节点的某个子节点,寻找的规则就是跟 css 选择器相同的,也就是 id 对应 # ;class 对应的就是 . 。很简单。

下面的重点是怎样测试 useState 或其他的 Hooks ,因为我使用的是 Hooks ,现在我已经不实用 class component 了,只不过我们的项目里面是有的,所以到时候这里也会有,最主要的原因是 class component 比较简单,而 hooks 的不是很清楚。

有一种简单的方法,也就是看表现,比如说输入框里面的变化与 useState 里面的值有绑定,那么我们只需要看输入框里面的 value 来看是否正常,或者是其他的显示的内容,比如我这个就是 h1 标签的值。

下面是类组件的方法。

class App extends React.PureComponent {
  state = {
    count: 0,
  };

  componentDidMount() {
    this.init();
  }

  init = () => {
    setTimeout(() => {
      this.setState(() => ({ count: 10 }));
    }, 5000);
  };
  add = () => {
    this.setState((lastState) => {
      return {
        count: lastState.count + 1,
      };
    });
  };
  sub = () => {
    this.setState((lastState) => {
      return {
        count: lastState.count - 1,
      };
    });
  };
  render() {
    const { count } = this.state;
    return (
      <div className="App">
        <h1>{count}</h1>
        <button className={"button"} onClick={this.add}>
          +
        </button>
        <button className={"button"} onClick={this.sub}>
          -
        </button>
      </div>
    );
  }
}

前面的步骤是一样的,也就是也需要先配置适配器:

import App from "./App";
import Enzyme, { shallow } from "enzyme";
import Adapter from "enzyme-adapter-react-16";

Enzyme.configure({ adapter: new Adapter() });

测试用例:

describe("测试 class 组件", () => {
  let wrapper;
  beforeAll(() => {
    wrapper = shallow(<App />);
  });

  beforeEach(() => {
    console.log("所有测试开始之前");
    expect(wrapper).toBeTruthy();
  });

  it("测试 Class Component 中的 button 的 click 方法,并检查 state", () => {
    wrapper.find("button").first().simulate("click");
    expect(wrapper.state().count).toBe(1);
  });
  
  it("测试 Class Component 中的 button 的 click 方法,并检查 state sub", () => {
    wrapper.find(".button").get(1).props.onClick();
    expect(wrapper.state().count).toBe(0);
  });
});

其中 describe 就是分组,这个也可以嵌套,比如说你的一个测试文件可以要分别测试类的生命周期方法,自己写的方法,还有 state 和 props 等相关,这个时候你可以分组测试,使用这个关键词就可以。 beforeAll 这个呢就是 describe 这个分组开始测试之前执行,如果直接写到文件中,也就是当前文件测试之前执行,这个只会在当前分组执行一次; beforeEach 这个就是每一个测试开始之前都要执行,和这些对应的还有 afterAll 和 afterEach 等相关方法,都是字面意思,也就是测试之后执行, afterAll 所有测试执行完成后(当前分组或全局), afterEach 每一个测试执行完成都会执行。

生命周期的测试方法可以先示例化类组件的方式进行,在 enzyme 中有一个方法 instance ,通过调用这个方法来进行示例化,然后就可以愉快的调用里面的方法了。 但是测试生命周期函数里面的方法,我现在还没有学到手,以后会更新。我的疑惑是:

componentDidMount() {
  this.init();
}
init = () => {
  setTimeout(() => {
    this.setState(() => ({ count: 10 }));
  }, 5000);
};

我想看看执行了这个方法,我的 state 里面的 count 方法是不是发生改变,目前还没有找到合适的方法测试,如果不小心被知道的你看到了,希望你能告诉我,让我不用到处找方法。

下次更新会包含 mock 的相关知识,我发现这一块目前我还是不理解。