【补充】 发现一篇不错的教程,专门针对 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 的相关知识,我发现这一块目前我还是不理解。