加载中

In this article we will take a look at using Jest, a testing framework maintained by Facebook, to test our ReactJS components. We’ll look at how we can use Jest first on plain JavaScript functions, before looking at some of the features it provides out of the box specifically aimed at making testing React apps easier. It’s worth noting that Jest isn’t aimed specifically at React — you can use it to test any JavaScript applications. However, a couple of the features it provides come in really handy for testing user interfaces, which is why it’s a great fit with React.

Jest是一个由Facebook维护的测试框架,在这篇文章中我们将学习一下如何使用Jest来测试我们的ReactJS组件。在学习它提供的使测试React apps更容易的新特性之前,我们先了解一下如何用Jest测试简单的JavaScript函数,Jest没有特别针对React,你可以用它测试任何JavaScript程序。然而,它提供了一些特性便于测试用户接口,这也是为什么它非常适合React。

Sample Application

Before we can test anything, we need an application to test! Staying true to web development tradition, I’ve built a small todo application that we’ll use as the starting point. You can find it, along with all the tests that we’re about to write, on GitHub. If you’d like to play with the application to get a feel for it, you can also find a live demo online.

The application is written in ES2015, compiled using Webpack with the Babel ES2015 and React presets. I won’t go into the details of the build set up, but it’s all in the GitHub repo if you’d like to check it out. You’ll find full instructions in the README on how to get the app running locally. If you’d like to read more, the application is built using Webpack, and I recommend “A Beginner’s guide to Webpack” as a good introduction to the tool.

The entry point of the application, app/index.js, which just renders the Todos component into the HTML:

render(
  <Todos />,
  document.getElementById('app')
);

The Todos component is the main hub of the application. It contains all the state (hard coded data for this application — in reality this would likely come from an API or similar) and has code to render the two child components: Todo, which is rendered once for each todo in the state, and AddTodo, which is rendered once and provides the form for a user to add a new todo.

Because the Todos component contains all the state, it needs the Todo and AddTodo components to notify it whenever anything changes. Therefore it passes functions down into these components that they can call when some data changes, and Todos can update the state accordingly.

示例应用程序

在测试之前,我们先得有一个应用程序来做测试才行! 秉承WEB开发的优良传统,我整了一个待办事项应用程序来做起步阶段的练手项目。在 GitHub 上你可以找到这个项目,我把它跟所有的测试代码放到了一起。如果想要先用用这个应用程序,好让你自己能具体了解到它可以做些什么,那么,你可以点击这里看到一个在线运行的示例版本。

程序是用 ES2015 写的, 编译时要用到预置了 ES2015 和 React 的 Webpack。关于构建的细节我不会讲太多,如果你想了解更多的话,这些资源都在 GitHub 上。关于如何在本地让项目运行起来,在 README 中有完整的说明。程序是使用 Webpack 构建的,所以如果你想要了解更多的东西的话, 我的建议是读一读《Webpack 入门指南》,它对这个工具有很好的介绍。

app/index.js 是这个应用程序的入口, 它只是将 Todos 组件渲染到了 HTML 里面:

render(
  <Todos />,
  document.getElementById('app')
);

Todos 组件是这个程序的重中之重,它包含了所有的状态(在这个程序里面是硬编码的——实际应用中要从一个API或者是其它类似的来源那里获取才好),还有一些代码是用来渲染两个子组件的: Todo, 在状态里面的每一个待办事项都会让它被渲染一遍,还有就是 AddTodo, 它会被渲染一次,提供给用户一个表单来添加新的待办事项。

因为 Todos 组件包含了所有的状态, 所以就需要 Todo 和 AddTodo 组件来通知它啥时候有改变发生。由此它会在某些数据发生改变时给这些组件下发一些函数来调用,而 Todos 也就能够有依据地对状态进行更新。

Finally, for now, you’ll notice that all the business logic is contained in app/state-functions.js.

export function toggleDone(state, id) {...}

export function addTodo(state, todo) {...}

export function deleteTodo(state, id) {...}

These are all pure functions that take the state and some data, and return the new state. If you’re unfamiliar with pure functions, they are functions that only reference data they are given and have no side effects. For more, you can read my article on A List Apart on pure functions and my article on SitePoint about pure functions and React.

If you’re familiar with Redux, they are fairly similar to what Redux would call a reducer. In fact, if this application got much bigger I would consider moving into Redux for a more explicit, structured approach to data. But for this size application you’ll often find that local component state and some well abstracted functions to be more than enough.

    最后,也就是现在,你会发现,所有的业务逻辑都包含在了app/state-functions.js中。

export function toggleDone(state, id) {...}

export function addTodo(state, todo) {...}

export function deleteTodo(state, id) {...}

    这些都是纯函数,它们可以接受状态和一些数据,并返回新的状态。如果你对纯函数不太熟悉的话,他们只是提供一些参考数据的函数,并没有其它什么副作用。要了解更多相关内容,你可以阅读我在A List Apart上面写的有关纯函数的文章以及SitePoint上有关纯函数和React的文章

如果你熟悉Redux,他们和Redux中称为减速器的东西非常类似。事实上,如果应用程序的规模变得很大,我会考虑转而使用Redux,因为这样可以得到一个更明确的、结构化的数据。但对于这种规模的应用程序,你通常会发现,本地组件状态并具有良好的抽象功能,这才是最重要的。

To TDD or not to TDD?

There have been many articles written on the pros and cons of Test Driven Development, where developers are expected to write the tests first, before writing the code to fix the test. The idea behind this is that by writing the test first, you have to think about the API that you’re writing and it can lead to a better design. For me I find that this very much comes down to personal preference and also to the sort of thing I’m testing. I’ve found that for React components I like to write the components first and then add tests to the most important bits of functionality. However, if you find that writing tests first for your components fits your workflow, then you should do that. There’s no hard rule here; do whatever feels best for you and your team.

Introducing Jest

Jest was first released in 2014 and although it initially garnered a lot of interest, the project was dormant for a while and not so actively worked on. However, Facebook has invested the last year into improving Jest and recently published a few releases with impressive changes that make it worth reconsidering. The only resemblance of Jest compared to the initial open source release is the name and the logo, everything else has been changed and rewritten. If you’d like to find out more about this, you can read Christoph Pojer’s comment where he discusses the current state of the project.

If you’ve been frustrated by setting up Babel, React and JSX tests using another framework, then I definitely recommend giving Jest a try. If you’ve found your existing test set up to be slow, I also highly recommend Jest. It automatically runs tests in parallel, and its watch mode is able to run only tests relevant to the changed file, which is invaluable when you have a large suite of tests. It comes with JSDom configured, meaning you can write browser tests but run them through Node, can deal with asynchronous tests and has advanced features such as mocking, spies and stubs built in.

用还是不用 TDD 方法呢?

许多的文章都描述过有关于测试驱动开发(Test Driven Development)的利弊,它所倡导的就是开发者实现写好测试代码,然后再编写代码来满足这些测试的要求。这个创意的背后意义就是通过先把测试写好,让你不得不去深入思考你所要编写的API,如此就可以得到一个更好的设计。在我看来,这很多都是要因人而定的,而且也要看测试的是什么东西。我发现对于 React组件来说,自己更喜欢先把组件写好,然后再对功能最重要的部分添加一些测试。不过不过你发现先给组件写好测试代码适合于自己的工作流程,那么就应该这样做,其实没啥硬性规定的。只要对你和你的团队有利,怎样都好。

Jest 介绍

Jest 第一次发布是在 2014 年,虽然它在最初获得了很多关注,但后面沉寂了一段时间没有进行积极的研发。不过在去年由 Facebook 发起的投资,对 Jest 进行改善,并且在最近发布了一个新的版本,带来了一些令人印象深刻的改变,重新引发了我们的关注。同最开始的开放源代码版本作比较,Jest与其的相似之处只有名称和logo,而其它所有的东西都已经改变和重写了。如果你想了解更多,可以读一读 Christoph Pojer 的评论,里面有他对项目当前状态的讨论。

如果你在使用其它框架对  Babel, React 和 JSX 进行测试时遇到过挫折,那么我绝对会推荐你尝试一下 Jest。如果你发现现有的测试设置太慢了,我也会强烈推荐Jest。它可以自动地让测试并行运行,这个在当你有一个很大的测试项目要跑的时候会很有价值。它附带有对 JSDom 的配置, 意味着你可以在浏览器里面编写测试,然后通过 Node 将它们跑起来,如此即可应对异步功能的测试,并且它还拥有一些高级的功能,比如内置的MOCK测试,SPY测试以及STUB测试。

Installing and Configuring Jest

To start with we need to get Jest installed. Because we’re also using Babel we’ll install another couple of modules that make Jest and Babel play nicely out of the box:

npm install --save-dev babel-jest babel-polyfill babel-preset-es2015 babel-preset-react jest

You also need to have a .babelrc file with Babel configured to use any presets and plugins you need. The sample project already has this file, which looks like so:

{
  "presets": ["es2015", "react"]
}

We won’t install any React testing tools yet, because we’re not going to start with testing our components, but our state functions.

Jest expects to find our tests in a __tests__ folder, which has become a popular convention in the JavaScript community, and it’s one we’re going to stick to here. If you are not a fan of the __tests__ setup, out of the box Jest also supports finding any .test.js and .spec.js files too.

As we’ll be testing our state functions, go ahead and create __tests__/state-functions.test.js.

We’ll write a proper test shortly, but for now put in this dummy test, which will let us check everything’s working correctly and we have Jest configured.

describe('Addition', () => {
  it('knows that 2 and 2 make 4', () => {
    expect(2 + 2).toBe(4);
  });
});

Now, head into your package.json. We need to set up npm test so that it runs Jest, and we can do that simply by setting the test script to run jest.

"scripts": {
  "test": "jest"
}

If you now run npm test locally, you should see your tests run, and pass!

PASS  __tests__/state-functions.test.js
  Addition
    ✓ knows that 2 and 2 make 4 (5ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 passed, 0 total
Time:        3.11s

If you’ve ever used Jasmine, or most testing frameworks, the above test code itself should be pretty familiar. Jest lets us use describe and it to nest tests as we need to. How much nesting you use is up to you; I like to nest mine so all the descriptive strings passed to describe and it read almost as a sentence.

When it comes to making actual assertions, you wrap the thing you want to test within an expect() call, before then calling an assertion on it. In this case we’ve used toBe. You can find a list of all the available assertions on the Jest documentation. toBe checks that the given value matches the value under test, using === to do so. We’ll meet a few of Jest’s assertions through this tutorial.

安装和配置Jest

首先我们需要安装Jest。因为要同时使用Babel,所以我们还需安装另外几个模块,以使得Jest和Babel可以协同工作:

npm install --save-dev babel-jest babel-polyfill babel-preset-es2015 babel-preset-react jest

你还需要为Babel配置一个.babelrc文件,这样可以使用所需的任何预设及插件。示例项目已经包含了该文件,就像:

{
  "presets": ["es2015", "react"]
}

我们还没有安装任何的React测试工具,因为我们还不打算开始测试我们的部件,但是我们的状态函数除外。

Jest希望在__tests__文件夹中找到我们的测试,这已成为JavaScript社区的惯例,我们也打算坚持这样做。如果你不喜欢__tests__这个设置,Jest也支持寻找任意的.test.js和.spec.js文件。

因为我们要测试我们的状态函数,所以继续下一步,创建__tests__/state-functions.test.js。

然后我们将编写一个测试文件,不过现在要进行dummy测试,这样可以检查所有工作是否正常以及对Jest的配置情况。

describe('Addition', () => {
  it('knows that 2 and 2 make 4', () => {
    expect(2 + 2).toBe(4);
  });
});

现在,进入你的package.json。我们需要设置npm test,以便它运行Jest,只需通过设置test脚本来运行Jest就可以。

"scripts": {
  "test": "jest"
}

如果你现在在本地运行npm test,你可以看到测试运行正常,并通过。

PASS  __tests__/state-functions.test.js
  Addition
    ✓ knows that 2 and 2 make 4 (5ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 passed, 0 total
Time:        3.11s

如果你曾经使用过Jasmine,或者大多数测试框架,那么对于以上的测试代码你应该相当熟悉。Jest让我们根据需要进行usedescribeanditto nest测试。至于要使用多少个nest完全取决于你了;我喜欢所有的描述性字符可以通过,todescribeanditread几乎是一个完整的句子。

当要进行实际的声明时,你会希望在此之前通过expect()调用进行测试。在这种情况下,我们使用toBe。你可以在Jest文档找到所有可用的声明列表,toBe会使用===检查给定的值是否匹配测试中的值。在本教程中,我们会遇到几个Jest的声明。

Testing Business Logic

Now we’ve seen Jest work on a dummy test, let’s get it running on a real one! We’re going to test the first of our state functions, toggleDone. toggleDone takes the current state and the ID of a todo that we’d like to toggle. Each todo has a done property, and toggleDone should swap it from true to false, or vice-versa.

If you’re following along with this, make sure you’ve cloned the repo and have copied the app folder to the same directory that contains your ___tests__ folder. You’ll also need to install the shortid package (npm install shortid --save) which is a dependency of the Todo app.

I’ll start by importing the function from app/state-functions.js, and setting up the test’s structure. Whilst Jest allows you to use describe and it to nest as deeply as you’d like to, you can also use test, which will often read better. test is just an alias to Jest’s it function, but can sometimes make tests much easier to read and less nested.

For example, here’s how I would write that test with nested describe and it calls:

import { toggleDone } from '../app/state-functions';

describe('toggleDone', () => {
  describe('when given an incomplete todo', () => {
    it('marks the todo as completed', () => {
    });
  });
});

And here’s how I would do it with test:

import { toggleDone } from '../app/state-functions';

test('toggleDone completes an incomplete todo', () => {
});

The test still reads nicely; but there’s less indentation getting in the way now. This one is mainly down to personal preference; choose whichever style you’re more comfortable with.

Now we can write the assertion. First we’ll create our starting state, before passing it into toggleDone, along with the ID of the todo that we want to toggle. toggleDone will return our finish state, which we can then assert on:

const startState = {
  todos: [{ id: 1, done: false, name: 'Buy Milk' }]
};

const finState = toggleDone(startState, 1);

expect(finState.todos).toEqual([
  { id: 1, done: true, name: 'Buy Milk' }
]);

Notice now that I use toEqual to make my assertion. You should use toBe on primitive values, such as strings and numbers, but toEqual on objects and arrays. toEqual is built to deal with arrays and objects, and will recursively check each field or item within the object given to ensure that it matches.

With that we can now run npm test and see our state function test pass:

PASS  __tests__/state-functions.test.js
  ✓ tooggleDone completes an incomplete todo (9ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 passed, 0 total
Time:        3.166s

测试业务逻辑

当看到Jest可以在模拟测试上运行时,接着就可以在真实案例上运行了!接下来测试的是第一个状态方法,toggleDone。toggleDone的参数是即将切换状态的todo的当前状态和它的ID。每个todo都有一个done属性,toggleDone方法应该将它由true改为false,或者相反的变更。

如果你在按本文步骤操作的话,请确保你已经克隆这个 仓库,并已拷贝到和你的___tests__文件夹同一个父文件夹下。同时也需要安装shortid包,(npm install shortid --save) 这个Todo app所依赖的包。

先从app/state-functions.js中引入待测试方法,然后建立测试结构。虽然Jest允许无限级的嵌套调用dsecribe和it,你还可以用test方法,因为它往往看起来更容易阅读。Jest中的test方法只是it的一个别名,但是有时可以让测试代码更易于阅读也不用很多的嵌套。

使用嵌套的describe和it方法,我可能写出如下案例:

import { toggleDone } from '../app/state-functions';

describe('toggleDone', () => {
  describe('when given an incomplete todo', () => {
    it('marks the todo as completed', () => {
    });
  });
});

而用test则可能是下面这个样子:

import { toggleDone } from '../app/state-functions';

test('toggleDone completes an incomplete todo', () => {
});

test方法的看起来更好,但是缩进可能会少一点。这主要与个人喜好相关,你可以选一个你更习惯的书写方式。

接着就可以写断言了。首先,在传入toggleDone之前先创建一个初始状态,同时传入需要转换状态的todo的ID。基于toggleDone方法返回的完成状态,我们这样写断言:

const startState = {
  todos: [{ id: 1, done: false, name: 'Buy Milk' }]
};

const finState = toggleDone(startState, 1);

expect(finState.todos).toEqual([
  { id: 1, done: true, name: 'Buy Milk' }
]);

注意这里是用toEqual作的断言。对于原始类型,如字符串和数字,应该使用toBe方法。toEqual是用于处理数字和对象的,它会递归的比较给定对象的所有字段或者元素以确认相等。

至此,我们便可以运行npm test,然后可以看到这个状态方法通过了测试:

PASS  __tests__/state-functions.test.js
  ✓ tooggleDone completes an incomplete todo (9ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 passed, 0 total
Time:        3.166s

Rerunning Tests on Changes

It’s a bit frustrating to make changes to a test file and then have to manually run npm test again. One of Jest’s best features is its watch mode, which watches for file changes and runs tests accordingly. It can even figure out which subset of tests to run based on the file that changed. It’s incredibly powerful and reliable, and you’re able to run Jest in watch mode and leave it all day whilst you craft your code.

To run it in watch mode, you can run npm test -- --watch. Anything you pass to npm test after the first -- will be passed straight through to the underlying command. This means that these two commands are effectively equivalent:

  • npm test -- --watch

  • jest --watch

I would recommend that you leave Jest running in another tab, or terminal window, for the rest of this tutorial.

Before moving onto testing the React components, we’ll write one more test on another one of our state functions. In a real application I would write many more tests, but for the sake of the tutorial, I’ll skip some of them. For now, let’s write a test that ensures that our deleteTodo function is working. Before seeing how I’ve written it below, try writing it yourself and seeing how your test compares.

Show me the test

The test doesn’t vary too much from the first, we set up our initial state, run our function and then assert on the finished state. If you left Jest running in watch mode, notice how it picks up your new test and runs it, and how quick it is to do so! It’s a great way to get instant feedback on your tests as you write them.

The tests above also demonstrate the perfect layout for a test, which is:

  • Set up

  • Execute the function under test

  • Assert on the results

By keeping the tests laid out in this way, you’ll find them easier to follow and work with.

Now we’re happy testing our state functions, let’s move onto React components.

变更后重新运行测试

更改测试文件,需要再次手动运行 npm test,这点令人沮丧。Jest 的最佳功能是在观察模式,它会观察文件的更改来运行测试。它依据哪些文件被更改,找出哪些子集需要测试运行。这令人难以置信的强大和可靠,用户可以把 Jest 运行在观察模式,然后将其放在一边,之后的一整天都可以只专注于修改代码了。

在观察模式下运行,可以运行 npm test -- --watch。在 npm test 之后,第一个 -- 之前,将命令直接传递给底层。以下这两个命令是等效的:

  • npm test -- --watch

  • jest --watch

我建议把 Jest 的运行留在另一个 tab 或者终端窗口中。

在测试 React 组件之前,我们对其中一个状态函数编写一个测试。在真实的应用程序中可能需要编写很多测试,但是为了本教程,我跳过了他们。现在,让我们编写一个测试,确保我们的 deleteTodo 函数工作。你可以尝试自己编写并与你自己的测试进行比较。

显示测试

测试的第一部分改变不多,我们设置了初始状态运行我们的函数并完成维护状态。如果你离开 Jest 运行着的观察者模式,那么你的新测试将不能被检测到,也不会被运行,更不能像在 Jest 中那么快!在你测试,编写它们的时候,这种获得即时反馈的方式是很伟大的。

上面的测试证明了对一个测试的完美安排是这样的:

  • 建立

  • 在测试之下执行函数

  • 反馈结果(结果断言)

以这种方式通过测试,你会发现他们更容易被跟踪和处理。

现在我很乐于测试我们的状态函数,让我们把目光转移到 React 组件吧。

Testing React Components

It’s worth noting that by default I would actually encourage you to not write too many tests on your React components. Anything that you want to test very thoroughly, such as business logic, should be pulled out of your components and sit in standalone functions, just like the state functions that we tested earlier. That said, it is useful at times to test some React interactions (making sure a specific function is called with the right arguments when the user clicks a button, for example). We’ll start by testing that our React components render the right data, and then look at testing interactions. Then we’ll move onto snapshots, a feature of Jest that makes testing the output of React components much more convenient.

To do this, we’ll need to make use of react-addons-test-utils, a library that provides functions for testing React. We’ll also install Enzyme, a wrapper library written by AirBnB that makes testing React components much easier. We’ll use this API throughout our tests. Enzyme is a fantastic library and the React team even recommend it as the way to test React components.

npm install --save-dev react-addons-test-utils enzyme

Let’s test that the Todo component renders the text of its todo inside a paragraph. First we’ll create __tests__/todo.test.js, and import our component:

import Todo from '../app/todo';
import React from 'react';
import { mount } from 'enzyme';

test('Todo component renders the text of the todo', () => {
});

I also import mount from Enzyme. The mount function is used to render our component and then allow us to inspect the output and make assertions on it. Even though we’re running our tests in Node, we can still write tests that require a DOM. This is because Jest configures jsdom, a library that implements the DOM in Node. This is great because we can write DOM based tests without having to fire up a browser each time to test them.

测试React组件

出于其意义甚微,事实上我一般不推荐为React组件写太多的测试。你需要测试的东西,比如业务逻辑,本来就应该从组件中抽离出去,然后落实到独立的方法中去,比如我们上文测试的状态方法。也即是说,测试React的交互(比如,确保当用户点击一个按钮是指定的方法会以正确的参数被调用)只是偶尔才有用。我们将从测试React组件可以渲染正确的数据开始测试,然后测试交互。然后进行的是快照,Jest的一个可以使测试React组件输出变得方便的特性。

因此我们必须使用react-addons-test-utils,一个提供了测试React方法的库。还需要安装EnzymeAirBnB写的一个可以使测试React组件变得简单的包装库。我们将在所有的测试中使用这个API。Enzyme是一个十分杰出的库,甚至React团队都在推荐用它测试React组件。

npm install --save-dev react-addons-test-utils enzyme

我们将测试Todo组件能否将它的todo文本渲染到一个段落。首先要创建__tests__/todo.test.js,然后导入我们的组件:

import Todo from '../app/todo';
import React from 'react';
import { mount } from 'enzyme';

test('Todo component renders the text of the todo', () => {
});

这里也从Enzyme中导入了mount。mount方法是用于渲染组件,其输出则用于检查和断言。虽然是在Node中测试,我们却可以在写测试时引入DOM元素。因为Jest配置了jsdom,一个在Node中实现了DOM的库。这是一个很棒的库,通过它我们就可以写基于DOM的测试,且在测试的时候不用每次都打开浏览器。

We can use mount to create our Todo:

const todo = { id: 1, done: false, name: 'Buy Milk' };
const wrapper = mount(
  <Todo todo={todo} />
);

And then we can call wrapper.find, giving it a CSS selector, to find the paragraph that we’re expecting to contain the text of the Todo. This API might remind you of jQuery, and that’s by design. It’s a very intuitive API for searching rendered output to find the matching elements.

const p = wrapper.find('.toggle-todo');

And finally, we can assert that the text within it is Buy Milk:

expect(p.text()).toBe('Buy Milk');

Which leaves our entire test looking like so:   

import Todo from '../app/todo';
import React from 'react';
import { mount } from 'enzyme';

test('TodoComponent renders the text inside it', () => {
  const todo = { id: 1, done: false, name: 'Buy Milk' };
  const wrapper = mount(
    <Todo todo={todo} />
  );
  const p = wrapper.find('.toggle-todo');
  expect(p.text()).toBe('Buy Milk');
});

 

Phew! You might think that was a lot of work and effort to check that ‘Buy Milk’ gets placed onto the screen and well…you’d be correct. Hold your horses for now though; in the next section we’ll look at using Jest’s snapshot ability to make this much easier.

In the meantime, let’s look at how you can use Jest’s spy functionality to assert that functions are called with specific arguments. This is useful in our case because we have the Todo component which is given two functions as properties, which it should call when the user clicks a button or performs an interaction.

In this test we’re going to assert that when the todo is clicked, the component will call the doneChange prop that it’s given.

test('Todo calls doneChange when todo is clicked', () => {
});

What we want to do is to have a function that we can keep track of its calls, and the arguments that it’s called with. Then we can check that when the user clicks the todo, the doneChange function is called and also called with the correct arguments. Thankfully, Jest provides this out of the box with spies. A spy is a function whose implementation you don’t care about; you just care about when and how it’s called. Think of it as you spying on the function. To create one we call jest.fn():

const doneChange = jest.fn();

This gives a function that we can spy on and make sure it’s called correctly. Let’s start by rendering our Todo with the right props:

const todo = { id: 1, done: false, name: 'Buy Milk' };
const doneChange = jest.fn();
const wrapper = mount(
  <Todo todo={todo} doneChange={doneChange} />
);

Next, we can find our paragraph again, just like in the previous test:

const p = TestUtils.findRenderedDOMComponentWithClass(rendered, 'toggle-todo');

And then we can call simulate on it to simulate a user event, passing click as the argument:

p.simulate('click');

And all that is left to do is assert that our spy function has been called correctly. In this case, we’re expecting it to be called with the ID of the todo, which is 1. We can use expect(doneChange).toBeCalledWith(1) to assert this, and with that we’re done with our test!

test('TodoComponent calls doneChange when todo is clicked', () => {
  const todo = { id: 1, done: false, name: 'Buy Milk' };
  const doneChange = jest.fn();
  const wrapper = mount(
    <Todo todo={todo} doneChange={doneChange} />
  );

  const p = wrapper.find('.toggle-todo');
  p.simulate('click');
  expect(doneChange).toBeCalledWith(1);
});

可以用mount方法创建Todo:

const todo = { id: 1, done: false, name: 'Buy Milk' };
const wrapper = mount(
  <Todo todo={todo} />
);

然后调用wrapper.find方法,传入一个CSS选择器,用于查找需要包含Todo内容的段落。这个API可能会让你想起jQuery,因为它是故意这么设计的。在渲染的输出中查找匹配的元素用时使用这个API看起来十分直观:

const p = wrapper.find('.toggle-todo');

最后,断言它包含的文本是Buy Milk:

expect(p.text()).toBe('Buy Milk');

完整的测试代码将会是下面这个样子:

import Todo from '../app/todo';
import React from 'react';
import { mount } from 'enzyme';

test('TodoComponent renders the text inside it', () => {
  const todo = { id: 1, done: false, name: 'Buy Milk' };
  const wrapper = mount(
    <Todo todo={todo} />
  );
  const p = wrapper.find('.toggle-todo');
  expect(p.text()).toBe('Buy Milk');
});

 

哇哦!可能你觉得为了测试‘Buy Milk’可以显示到正确的位置的确需要很多的工作,呃……确实如此。但是先不必着急,下一节将会看到用Jest的快照功能会使这个测试变简单很多。

此时请先看一下,如何用Jest的监听功能来断言方法是否以指定参数调用。对于本例来说这个测试是很有用的,因为我们的Todo组件包含了两个函数类型的属性,它们是在用户点击按钮或者触发交互时被调用的。

下面的测试会断言当点击todo按钮的时候,会调用给定组件的doneChange方法。

test('Todo calls doneChange when todo is clicked', () => {
});

现在需要有个可以跟踪方法调用以及调用时的参数的函数。然后就可以检查当用户点击todo时,doneChange会被以正确的参数调用。幸好,Jest提供了开箱即用的监听功能。监听方法是无需关注其实现,只需考虑其调用时机及方式的方法。可以想象为你在监视那个方法。可以调用Jest.fn()创建:

const doneChange = jest.fn();

返回的方法可以用于监听以检查其是否被正确调用。一开始要渲染一个有正确属性的Todo:

const todo = { id: 1, done: false, name: 'Buy Milk' };
const doneChange = jest.fn();
const wrapper = mount(
  <Todo todo={todo} doneChange={doneChange} />
);

接着,再次查找那个段落,如同上一个测试:

const p = TestUtils.findRenderedDOMComponentWithClass(rendered, 'toggle-todo');

然后可以调用它的simulate方法,模拟用户事件,传入参数click:

p.simulate('click');

最后只需断言监视方法可以被正确调用。本例中,期望调用时传入todo的ID也就是1。可以用expect(doneChange).toBeCalledWith(1)来断言,此时也完成了我们的测试!

test('TodoComponent calls doneChange when todo is clicked', () => {
  const todo = { id: 1, done: false, name: 'Buy Milk' };
  const doneChange = jest.fn();
  const wrapper = mount(
    <Todo todo={todo} doneChange={doneChange} />
  );

  const p = wrapper.find('.toggle-todo');
  p.simulate('click');
  expect(doneChange).toBeCalledWith(1);
});

Better Component Testing with Snapshots

I mentioned above that this might feel like a lot of work to test React components, especially some of the more mundane functionalities (such as rendering the text). Rather than make a large amount of assertions on React components, Jest lets you run snapshot tests. These are not so useful for interactions (in which case I still prefer a test like we just wrote above), but for testing that the output of your component is correct, they are much easier.

When you run a snapshot test, Jest renders the React component under test and stores the result in a JSON file. Every time the test runs, Jest will check that the React component still renders the same output as the snapshot. Then, when you change a component’s behaviour, Jest will tell you and either:

  • You’ll realise you made a mistake, and you can fix the component so it matches the snapshot again.

  • Or, you made that change on purpose, and you can tell Jest to update the snapshot.

This way of testing means that:

  • You don’t have to write a lot of assertions to ensure your React components are behaving as expected.

  • You can never accidentally change a component’s behaviour, because Jest will realise.

You also don’t have to snapshot all your components; in fact I’d actively recommend against it. You should pick components with some functionality that you really need to ensure is working. Snapshotting all your components will just lead to slow tests that aren’t useful. Remember, React is a very thoroughly tested framework, so we can be confident that it will behave as expected. Make sure you don’t end up testing the framework, rather than your code!

使用组件的快速测试更好

我上面提及的看起来测试React组件需要大量工作,特别是一些比较通用功能(比如渲染文本)。Jest允许你快速测试,而不是在React组件里使用大量断言。这些对于交互没有多大用处(但是我依然选择使用我上面提及的),但是对于测试组件的输出是否正确,它使用起来非常简单。

当你运行一个快速测试,Jest在测试时渲染React组件,并且益JSON格式文件来存储结果。每次运行测试,Jest将会快速检测React组件是否依然渲染相同输出。然后,当你改变一个组件的行为,Jest将会告诉你下面的其中一种情况:

  • 你意识到了错误,同时你可以修复组件使之再次匹配快速测试。

  • 或者,你特意做出修改,然后通过Jest来更新快速测试。

这种测试方式意味着:

  • 你不需要写大量断言来确认你的React组件与你期待是否相符。

  • 你可以任意时刻改变组件方法,因为Jest会识别。

你也不用对你所有组件进行快速测试,事实上,我不推荐这种方式。你可以选择一些需要确认的组件进行功能测试。快速测试你所有组件将会使得你的测试周期变长,效率更低。请谨记,React是一个非常好用的测试框架,所以我们大可以对它的测试结果放心。请确认你没有结束测试框架,而不是你的代码。



返回顶部
顶部