Jest由Facebook引入,用于测试JavaScript,特别是React应用程序。它是目前测试React组件的最流行的方法之一。由于它有自己的测试运行器,你可以简单地从命令行调用Jest来运行你的所有测试。你的所有测试都被定义为测试套件(例如:describe-block)和测试案例(例如:it-block或test-block)。
Jest设置允许你添加可选的配置,自己引入设置程序,或定义自定义npm脚本来运行你的Jest测试。在本教程中,你将学习如何执行所有这些内容。除了所有的设置外,Jest还附带了丰富的测试断言API(例如,true等于true)。本教程将告诉你如何为你的React组件和JavaScript函数使用这些测试断言。此外,你还将学习快照测试来测试你的React组件。
React设置中的Jest测试
在实现测试设置和编写第一个React组件测试之前,你将需要一个简单的React应用程序,可以首先进行测试。从src/index.js文件开始,在这里你可以导入并渲染尚未实现的App组件。
import React from 'react';import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('app'));
你的src/App.js文件中的App组件将是一个带有ReactHooks的React功能组件。它使用axios作为第三方库,所以要确保在命令行中为你的React应用程序安装node包,npm install axios 。
import React from 'react';import axios from 'axios';
export const dataReducer = (state, action) => { if (action.type === 'SET_ERROR') { return { ...state, list: [], error: true }; }
if (action.type === 'SET_LIST') { return { ...state, list: action.list, error: null }; }
throw new Error();};
const initialData = { list: [], error: null,};
const App = () => { const [counter, setCounter] = React.useState(0); const [data, dispatch] = React.useReducer(dataReducer, initialData);
React.useEffect(() => { axios .get('http://hn.algolia.com/api/v1/search?query=react') .then(response => { dispatch({ type: 'SET_LIST', list: response.data.hits }); }) .catch(() => { dispatch({ type: 'SET_ERROR' }); }); }, []);
return ( <div> <h1>My Counter</h1> <Counter counter={counter} />
<button type="button" onClick={() => setCounter(counter + 1)}> Increment </button>
<button type="button" onClick={() => setCounter(counter - 1)}> Decrement </button>
<h2>My Async Data</h2>
{data.error && <div className="error">Error</div>}
<ul> {data.list.map(item => ( <li key={item.objectID}>{item.title}</li> ))} </ul> </div> );};
export const Counter = ({ counter }) => ( <div> <p>{counter}</p> </div>);
export default App;
React应用程序正在做两件事:
-
首先,它渲染了一个Counter组件,该组件接收道具来渲染一个计数器属性。计数器属性在App组件中被作为状态管理,有一个useState React Hook。此外,计数器的状态可以通过两个按钮更新,即增加和减少状态。
-
第二,App组件在第一次渲染时从第三方API 获取数据。这里我们使用React的useReducer Hook来管理数据状态--它要么是实际的数据,要么是错误。如果有错误,我们就渲染一个错误信息。如果有数据,我们将数据渲染成React组件中的项目列表。
注意,我们已经从文件中导出了我们的两个组件和reducer函数,以使它们在以后的测试文件中可以测试。这样一来,每个组件和减速器都可以被隔离测试--这对减速器函数测试从一个状态到另一个状态的状态转换特别有意义。这就是你所说的真正的单元测试。该函数用一个输入进行测试,测试断言一个预期输出。
此外,我们有两个React组件之间的关系,因为它们是父和子组件。这是另一种情况,可以作为集成测试来测试。如果你要孤立地测试每个组件,你会有单元测试。但是通过在它们的上下文中一起测试,例如将父组件和子组件一起渲染,你就可以对两个组件进行集成测试。
为了让我们的测试启动和运行,通过在命令行中安装Jest作为开发依赖来设置它。
npm install --save-dev jest
在你的package.json文件中,创建一个新的npm脚本,运行Jest。
{ ... "scripts": { "start": "webpack serve --config ./webpack.config.js --mode development", "test": "jest" }, ...}
此外,我们希望在用Jest编写的测试中拥有更多配置。因此,传递一个额外的Jest配置文件给你的Jest脚本。
{ ... "scripts": { "start": "webpack serve --config ./webpack.config.js --mode development", "test": "jest --config ./jest.config.json" }, ...}
接下来,我们可以在一个配置文件中为Jest定义这个可选配置。在命令行中创建它。
touch jest.config.json
在这个Jest配置文件中,添加以下测试模式匹配来运行所有应被Jest执行的测试文件。
{ "testRegex": "((\\.|/*.)(spec))\\.js?$"}
testRegex 配置是一个正则表达式,可以用来指定你的Jest测试所在的文件的命名。在这种情况下,这些文件的名称是:*spec.js 。这样你就可以将它们与*src/*文件夹中的其他文件明确分开。最后,在一个新的src/App.spec.js 文件中,在你的App组件的文件旁边添加一个测试文件。首先,在命令行中创建测试文件。
touch src/App.spec.js
其次,在这个新文件的测试套件中实现你的第一个测试案例。
describe('My Test Suite', () => { it('My Test Case', () => { expect(true).toEqual(true); });});
现在你应该能够运行npm test ,用你的测试用例执行你的测试套件。对于你之前的测试用例,测试应该是绿色的(有效的,成功的),但是如果你把测试改成其他的东西,比方说expect(true).toEqual(false); ,它应该是红色的(无效的,失败的)。恭喜你,你已经用Jest运行了你的第一个测试!
最后但并非最不重要的是,添加另一个npm脚本来观察你的Jest测试。通过使用这个命令,你可以让你的测试在一个命令行标签中持续运行,而你在另一个命令行标签中启动你的应用程序。每当你在开发你的应用程序时改变了源代码,你的测试将通过这个观察脚本再次运行。
{ ... "scripts": { "start": "webpack serve --config ./webpack.config.js --mode development", "test": "jest --config ./jest.config.json", "test:watch": "npm run test -- --watch" }, ...}
现在你可以在观察模式下运行你的Jest测试。这样做,你将有一个打开的终端标签,用于你的Jest测试在观察模式下与npm run test:watch ,一个打开的终端标签,用于启动你的React应用程序与npm start 。每当你改变一个源文件,你的测试应该再次运行,因为观察模式。
练习。
- 阅读更多关于开始使用Jest的信息
- 阅读更多关于Jest的Globals的信息
- 阅读更多关于Jest的断言的信息
React中的Jest快照测试
Jest引入了所谓的快照测试。基本上,当你运行测试时,快照测试会创建一个快照--它存储在一个单独的文件中--你的渲染组件的输出。当你再次运行你的测试时,这个快照被用来与下一个快照进行比较。如果你的渲染组件的输出发生了变化,两个快照的差异会显示出来,快照测试会失败。这一点也不坏,因为快照测试应该只在渲染组件的输出发生变化时通知你。在快照测试失败的情况下,你可以接受这些变化或拒绝这些变化,并修复你的组件关于其渲染输出的实现。
通过使用Jest的快照测试,你可以保持你的测试的轻量级,而不用太担心组件的实现细节。让我们看看这些在React中是如何工作的。首先,安装常用于Jest的react-test-renderer工具库,在测试中渲染你的实际组件。
npm install --save-dev react-test-renderer
第二,用Jest实现你的第一个快照测试。首先,用新的渲染器渲染一个组件,将其转化为JSON,并将快照与之前存储的快照相匹配。
import React from 'react';import renderer from 'react-test-renderer';
import { Counter } from './App';
describe('Counter', () => { test('snapshot renders', () => { const component = renderer.create(<Counter counter={1} />); let tree = component.toJSON(); expect(tree).toMatchSnapshot(); });});
现在再次以观察模式运行你的Jest测试:npm run test:watch 。在观察模式下运行你的测试,当有快照测试时,让你有机会与Jest互动地运行你的测试。例如,一旦你的观察模式被激活,在你的React组件中把div元素改为span元素。
export const Counter = ({ counter }) => ( <span> <p>{counter}</p> </span>);
在观察模式下运行的测试的命令行应该显示一个失败的快照测试。
Counter ✕ snapshot renders (21ms)
● Counter › snapshot renders
expect(received).toMatchSnapshot()
Snapshot name: `Counter snapshot renders 1`
- Snapshot + Received
- <div> + <span> <p> 1 </p> - </div> + </span>
Watch Usage: Press w to show more.
之前的快照与React组件的新快照不匹配了。此外,命令行为你提供了现在要做的事情(可选择按键盘上的w )。
Watch Usage › Press a to run all tests. › Press f to run only failed tests. › Press p to filter by a filename regex pattern. › Press t to filter by a test name regex pattern. › Press u to update failing snapshots. › Press i to update failing snapshots interactively. › Press q to quit watch mode. › Press Enter to trigger a test run.
按a 或f 将运行所有或只运行你失败的测试。如果你按下u ,你接受 "失败 "的测试是有效的,你的React组件的新快照将被存储。如果你不想接受它作为一个新的快照,那么就通过修复你的组件来修复你的测试。
export const Counter = ({ counter }) => ( <div> <p>{counter}</p> </div>);
之后,快照测试应该再次变成绿色。
PASS src/App.spec.js Counter ✓ snapshot renders (17ms)
Test Suites: 1 passed, 1 totalTests: 1 passed, 1 totalSnapshots: 1 passed, 1 totalTime: 4.311sRan all test suites related to changed files.
Watch Usage: Press w to show more.
总之,通过改变组件并接受新的快照或再次修复你的React组件,你可以自己试试。同时为你的App组件添加另一个快照测试。
import React from 'react';import renderer from 'react-test-renderer';
import App, { Counter } from './App';
describe('App', () => { test('snapshot renders', () => { const component = renderer.create(<App />); let tree = component.toJSON(); expect(tree).toMatchSnapshot(); });});
describe('Counter', () => { test('snapshot renders', () => { const component = renderer.create(<Counter counter={1} />); let tree = component.toJSON(); expect(tree).toMatchSnapshot(); });});
大多数时候,快照测试对每个React组件都是一样的。你渲染组件,将其渲染的输出转换为JSON以使其具有可比性,并将其与之前的快照相匹配。有了快照测试,就可以使React组件的测试更加轻巧。此外,快照测试可以完美地用于补充单元测试和集成测试,因为它们不明确测试任何实现逻辑。
注意:如果你在React中使用CSS-in-JS的Styled Components,请查看jest-styled-components,用快照测试来测试你的CSS样式定义。
练习。
- 检查你生成的src/snapshots/App.spec.js.snap文件
- 理解这个文件存在的原因,以及它是如何促进快照的相互比较的。
- 习惯于接受或拒绝(修复你的组件)快照
- 创建新的React组件并使用快照测试来测试它们
- 了解更多关于Jest快照测试的信息
React中的Jest单元/集成测试
Jest也可以用来测试你的JavaScript逻辑,作为集成或单元测试。例如,你的App组件通过使用React Hook来获取数据并将结果作为状态存储在一个reducer函数中。这个reducer函数被导出为独立的JavaScript函数,对React没有任何了解。因此,不需要对React组件进行任何渲染,我们可以把这个reducer函数作为普通的JavaScript函数来测试。
import React from 'react';import renderer from 'react-test-renderer';
import App, { Counter, dataReducer } from './App';
const list = ['a', 'b', 'c'];
describe('App', () => { describe('Reducer', () => { it('should set a list', () => { const state = { list: [], error: null }; const newState = dataReducer(state, { type: 'SET_LIST', list, });
expect(newState).toEqual({ list, error: null }); }); });
...});
写两个额外的测试来覆盖你的还原器函数的其他部分和边缘情况。这两个其他部分被称为 "不那么高兴 "的路径,因为它们不假设成功的结果(例如,数据获取失败)。通过这样写你的测试,你涵盖了你的应用程序逻辑中的所有条件路径。
import React from 'react';import renderer from 'react-test-renderer';
import App, { Counter, dataReducer } from './App';
const list = ['a', 'b', 'c'];
describe('App', () => { describe('Reducer', () => { it('should set a list', () => { const state = { list: [], error: null }; const newState = dataReducer(state, { type: 'SET_LIST', list, });
expect(newState).toEqual({ list, error: null }); });
it('should reset the error if list is set', () => { const state = { list: [], error: true }; const newState = dataReducer(state, { type: 'SET_LIST', list, });
expect(newState).toEqual({ list, error: null }); });
it('should set the error', () => { const state = { list: [], error: null }; const newState = dataReducer(state, { type: 'SET_ERROR', });
expect(newState.error).toBeTruthy(); }); });
...});
一旦你运行你的测试,你应该在命令行上看到以下输出。如果一个测试失败了,例如在观察模式下,你会立即被通知。
You should get a similar output:
PASS src/App.spec.js App ✓ snapshot renders (18ms) Reducer ✓ should set a list (7ms) ✓ should reset the error if list is set (1ms) ✓ should set the error Counter ✓ snapshot renders (19ms)
Test Suites: 1 passed, 1 totalTests: 5 passed, 5 totalSnapshots: 2 passed, 2 totalTime: 2.325sRan all test suites.
Watch Usage: Press w to show more.
你已经看到Jest也可以用来测试普通的JavaScript函数。它不需要只用于React。如果你的应用程序中有更复杂的函数,不要犹豫,把它们提取为独立的函数,可以导出,使其可测试。这样你就能保证你的复杂业务逻辑能够正常工作,因为它已经被你的Jest断言所覆盖。
练习。
Jest为您提供了测试React组件所需的(几乎)一切。你可以从命令行运行所有的测试,给它额外的配置,并在你的测试文件中定义测试套件和测试案例。快照测试给了你一个轻量级的方法来测试你的React组件,只需将渲染后的输出与之前的输出进行比较。此外,你已经看到Jest可以只用于测试JavaScript函数,所以它并不严格约束React测试。
然而,用Jest测试React组件的DOM是比较困难的。这就是为什么存在其他第三方库,如React测试库或Enzyme,使React组件单元测试成为可能。关注该系列教程,了解更多React的测试实例。