Skip to main content

使用 Jest 进行测试

Backstage用途玩笑满足我们所有的单元测试需求。

Jest 是 Facebook 专为 React 打造的单元测试框架,它沿袭了其他经典 Node.js 单元测试相关框架和库,如摩卡,茉莉花.

运行测试

运行所有测试:

yarn test

运行单个测试(例如MyComponent.test.tsx):

yarn test MyComponent

同时运行MyComponent.test.tsxMyControl.test.tsx测试套件:

yarn test MyCo

注意:如果console.logs请仅运行您正在进行的单项测试。这是 Jest 中的一个错误.

命名测试文件

测试应命名为[filename].test.ts[filename].test.tsx如果它包含 JSX(很多 React 测试都是这种情况,例如组件)。

例如,对**Link.tsx文件中存在Link.test.tsx**.

##第三方依赖

Jest 有自己的内置断言库,其中包括expect因此没有必要import不过,由于断言库会简单地抛出错误,如果需要,导入第三方库也是可行的(如 Chai 或西农).

我们使用轻便的React 测试库来呈现 React 组件。

测试工具

TODO.

编写单元测试

以下原则是判断你是否在编写高质量前端单元测试的良好指南。

错误的单元测试原则

没有单元测试比糟糕的单元测试更好。

编写糟糕的单元测试:

  • 给人一种你的代码比实际代码更安全或更可靠的错觉。 * 功能相当于一个糟糕的注释,因为它会导致下一个开发人员做出错误的假设。 * 由于无关的代码更改需要更新单元测试,因此会增加未来的工作。

输入/输出原则

单元测试验证输出与预期输入是否匹配。

对于后端,这意味着当您提供配置 X 时,对象会响应 Y;对于前端,这意味着当您为组件提供属性 X 时,可视化功能会响应 Y。

黑箱原理

一个好的单元测试不会告诉对象它应该如何工作,而应该 > 只比较输入和输出。

考虑一个表单的单元测试。 一个好的单元测试不会测试表单字段的顺序,而是验证表单字段的输入是否会在点击提交时导致某个后端调用。

可扩展性原则

单元测试的质量与代码变化的程度成正比。

这一点经常被忽视!单元测试不是验证代码是否从未改变的测试。 劣质的单元测试是这样编写的:每当您对代码进行微小改动时,您就必须更新单元测试。 一个好的单元测试套件允许在以下方面有很大的灵活性如何代码的编写方式,以便将来进行重构时无需触及原始单元测试。

复杂性递增原则

套件中单元测试的顺序应从最不具体到最具体。

Jest 会按照提供的顺序运行所有测试,而与describe()我们可以用它来帮助我们编写测试,以帮助下一个开发人员调试他们所破坏的东西。

这样做的目的是,如果他们破坏了一个单元测试,下一个开发人员应该能够从测试破坏的顺序中看出他们应该做些什么来修复问题。

例如,好的单元测试会在验证输出的测试之前,验证测试中函数的参数。 如果不测试这一点,那么仅仅抛出一个错误说输出不正确,就会让下一个开发人员认为他们可能破坏了对象的整个功能,而不是简单地让他们知道他们有一个无效的输入。

打破功能性原则

一般来说,单元测试不应该准确地测试输出是如何出现的,而 > 应该测试功能对 > 输入变化是否有预期的_一般_响应。

这与可扩展性原则相辅相成,主要适用于前端开发。 作为一般准则,前端应具有足够的灵活性,以便用户体验或设计可以改变,同时尽可能减少代码量。 例如,一个糟糕的单元测试会验证按钮悬停时的颜色。 这将是一个糟糕的单元测试,因为如果您决定测试按钮上稍有不同的颜色,单元测试就会失败。 一个更好的单元测试会验证按钮的 CSS 类名在悬停时是否分配正确,或者测试完全不同的内容。

示例:加载指示器

前端的一个经典单元测试是验证后端请求时是否显示加载指示器。

以下是我们可以在数据加载时测试的一些内容:

组件的内部 "加载 "状态是否发生了变化?

这并不是一个很好的测试,因为它并没有实际测试功能(向用户显示一条信息)是否真正实现。 它还违反了黑盒原则,因为它期望组件的内部结构必须以某种方式工作。 这本身可能是一个很好的测试,但它并没有真正实现验证输入(加载数据)是否实现、输出(向用户显示一条信息)是否实现的目标。

DOM 中是否出现了文本 "Loading!"?

这是一个更好的测试,因为它验证了功能,但却破坏了可扩展性原则。 通过测试'Loading...'如果我们想添加国际化或将信息更改为更具体的内容,我们就会中断测试,并且必须在两个地方更新代码。

<Loading /> 被加载了吗?

这是这些示例中最好的测试(根据您的实施情况,可能还有更多测试)。

验证<Loading />数据加载时加载的数据是最好的测试,因为它符合上述所有原则:

符合输入/输出原则:当输入发生变化时,验证输出是否发生变化

满足黑箱原则:无法验证如何<Loading />组件被安装,只是它是根据输入而被安装的。

满足可扩展性原则注:如果我们决定重构整个加载指示器的显示方式,测试仍可正常工作,无需触碰它。

满足功能性原则说明:此测试验证的是功能(显示指示器)是否正常工作,而不是如何工作。

复杂性递增原则并不适用于本示例,因此排除了它。 不过,如果要将此测试放在其他测试套件中,最好先测试组件在接到加载数据的指令后是否真的加载了数据,这样如果数据加载部分出现问题,两个测试都会失败,下一个开发人员就会立即知道问题出在数据加载部分,而不是加载指示器。

示例

实用功能

实用程序函数是一种没有副作用的函数,它接收参数并返回结果或显示错误信息或控制台信息,就像这样:

StringUtil ellipsis

export function ellipsis(text, maxLength, midCharIx = 0, ellipsis = '...') {
// Do something blackbox. We should not care about the internals,
// only inputs and outputs.
...
return someFinalValue;
}

在效用函数中,有四点需要检验:

处理无效输入 2.验证默认输入参数 3.验证预期输入参数的输出 4.处理抛出的错误

处理无效输入(处理抛出的错误):

it('Throws an error on improper arguments', () => {
expect(() => {
ellipsis();
}).toThrowError("Expected 'text' to be defined");
});

验证默认输入参数:

it('Works with defaults', () => {
expect(ellipsis('Hello world', 3)).toBe('Hel...');
expect(ellipsis('', 3)).toBe('');
expect(ellipsis('H', 3)).toBe('H');
expect(ellipsis('Hello', 5)).toBe('Hello');
});

验证预期输入参数的输出:

对于边缘情况尤其如此!

it('Works with midCharIx', () => {
expect(ellipsis('Hello world', 3, 6)).toBe('...o w...');
expect(ellipsis('', 3, 6)).toBe('');
expect(ellipsis('Backstage is amazing', 4, 10)).toBe('...e is...');
});

非 React 课程

测试一个 JavaScript 对象,该对象是不是React 组件与其他语言中的对象测试遵循的原则是一样的。

API 测试原则

测试应用程序接口需要验证四点:

1.无效输入在发送到服务器之前被捕获。 2.有效输入转化为有效的浏览器请求。 3.服务器响应转化为预期的 JavaScript 对象。 4.服务器错误得到优雅处理。

模拟 API 调用

玩笑中的嘲弄是指用一种替代方法包装现有函数(如 API 调用函数)。

例如

./MyApi.ts

export async function fetchSomethingFromServer() {
// Live production call to a URI. Must be avoided during testing!
return fetch('blah');
}

./__mocks__/MyApi.ts

export async function fetchSomethingFromServer() {
// Simulate a production call response
return 'some result object simulating server data here';
}

./MyApi.test.ts

// This import will actually return the contents of the file in the
// __mocks__ folder now, due to the jest.mock line below
import { fetchSomethingFromServer } from './MyApi';

// This instructs Jest to swap all imports of './MyApi.ts' to
// './__mocks__/MyApi.ts' - this gets automatically hoisted to the top
// of the file
jest.mock('./MyApi');

it('loads data', async () => {
await expect(fetchSomethingFromServer()).resolves.toBe(
'some result object simulating server data here',
);
});

React 组件

利用 React 生命周期开展工作

React 生命周期是异步的。

当您拨打setState或更新props请注意下面的示例:

class MyComponent extends Component {
load() {
this.setState({loading: true});
}

render() {
return this.state.loading ? <Loading /> : 'Finished!';
}
}

...

// INCORRECT
it('Test loading', () => {
const wrapper = mount(<MyComponent />);
wrapper.load();
expect(wrapper.find('Loading').length).toEqual(1); // Will fail
});

// CORRECT
it('Test loading', () => {
const wrapper = mount(<MyComponent />);
wrapper.load();
wrapper.update(); // This tells the components to run through a render cycle
expect(wrapper.find('Loading').length).toEqual(1);
});

欲了解更多信息:

访问 "存储"、"主题"、路由、浏览器历史记录等

Backstage 应用程序的根部有几个核心提供程序。 要在 "示例 "Backstage 应用程序中运行您的测试,可以使用我们的实用功能:

wrapInTestApp

import { wrapInTestApp } from '../../test-utils';
...
it('Definitely is not a coconut', () => {
const mangoWrapper = mount(wrapInTestApp(<Mango />));

expect(mangoWrapper.context().store).toBeDefined();
});

注:测试应用中的包装要求你做一个find()dive()因为封装后的组件现在就是应用程序。

调试 Jest 测试

您可以找到它这里