Gowhich

Durban's Blog

项目初始化

1
2
3
4
5
git clone https://github.com/durban89/webpack4-react16-reactrouter-demo.git 
cd webpack4-react16-reactrouter-demo
git fetch origin
git checkout v_1.0.32
npm install

与MongoDB一起使用(Using with MongoDB)

通过 Global Setup/Teardown和Async Test EnvironmentAPI,Jest可以与MongoDB一起顺利运行。

jest-mongodb实例
基本思路是:

.旋转内存中的mongodb服务器 jest.mongodb.setup.js
.使用mongo URI导出全局变量 jest.mongodb.environment.js
.使用真实数据库编写查询/聚合测试✨
.使用Global Teardown关闭mongodb服务器 jest.mongodb.teardown.js

这是GlobalSetup脚本的一个示例
在项目根目录添加如下几个文件

jest.mongodb.setup.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const path = require('path');
const fs = require('fs');
const MongodbMemoryServer = require('mongodb-memory-server');

const globalConfigPath = path.join(__dirname, 'globalConfig.json');
const mongoServer = new MongodbMemoryServer.MongoMemoryServer();

module.exports = async function setupMongodb() {
console.log('配置Jest Setup调用');
const mongoConfig = {
mongoDBName: 'jest',
mongoUri: await mongoServer.getConnectionString(),
};

// 将配置写入本地配置文件以供所有测试都能调用的到
fs.writeFileSync(globalConfigPath, JSON.stringify(mongoConfig));

// 设置对mongodb的引用,以便在拆卸期间关闭服务器。
global.__MONGOD__ = mongoServer;
};

jest.mongodb.environment.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const NodeEnvironment = require('jest-environment-node');
const path = require('path');
const fs = require('fs');

const globalConfigPath = path.join(__dirname, 'globalConfig.json');

class MongoEnvironment extends NodeEnvironment {
constructor(config) {
super(config);
}

async setup() {
console.log('设置MongoDB测试环境');

const globalConfig = JSON.parse(fs.readFileSync(globalConfigPath, 'utf-8'));

this.global.__MONGO_URI__ = globalConfig.mongoUri;
this.global.__MONGO_DB_NAME__ = globalConfig.mongoDBName;

await super.setup();
}

async teardown() {
console.log('卸载MongoDB测试环境');

await super.teardown();
}

runScript(script) {
return super.runScript(script);
}
}

module.exports = MongoEnvironment;

jest.mongodb.teardown.js

1
2
3
4
module.exports = async function tearDownMongodb() {
console.log('配置Jest TearDown调用');
await global.__MONGOD__.stop();
};

执行测试用例之前需要安装以下依赖库(如果还没有安装的情况下)

1
2
3
npm install mongodb-memory-server --save-dev
npm install mongodb --save-dev
npm install jest-environment-node --save-dev

修改jest.config.js文件,添加下面的代码

1
2
3
globalSetup: './jest.mongodb.setup.js',
globalTeardown: './jest.mongodb.teardown.js',
testEnvironment: './jest.mongodb.environment.js',

jest.config.js

1
2
3
4
5
6
7
module.exports = {
setupFiles: ['./jest.setup.js'],
snapshotSerializers: ['enzyme-to-json/serializer'],
globalSetup: './jest.mongodb.setup.js',
globalTeardown: './jest.mongodb.teardown.js',
testEnvironment: './jest.mongodb.environment.js',
};

下面看测试用例代码
src/__tests__/jest_mongodb.test.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
const {
MongoClient,
} = require('mongodb');

let connection;
let db;

beforeAll(async () => {
connection = await MongoClient.connect(global.__MONGO_URI__, {
useNewUrlParser: true,
});
db = await connection.db(global.__MONGO_DB_NAME__);
});

afterAll(async () => {
await connection.close();
});

it('从集合中汇总文档', async () => {
const files = db.collection('files');

await files.insertMany([{
type: 'Document',
},
{
type: 'Video',
},
{
type: 'Image',
},
{
type: 'Document',
},
{
type: 'Image',
},
{
type: 'Document',
},
]);

const topFiles = await files
.aggregate([{
$group: {
_id: '$type',
count: {
$sum: 1,
},
},
},
{
$sort: {
count: -1,
},
},
])
.toArray();

expect(topFiles).toEqual([{
_id: 'Document',
count: 3,
},
{
_id: 'Image',
count: 2,
},
{
_id: 'Video',
count: 1,
},
]);
});

实践项目地址

1
2
git clone https://github.com/durban89/webpack4-react16-reactrouter-demo.git
git checkout v_1.0.33

项目初始化

1
2
3
4
5
git clone https://github.com/durban89/webpack4-react16-reactrouter-demo.git 
cd webpack4-react16-reactrouter-demo
git fetch origin
git checkout v_1.0.32
npm install

ES6 Class Mocks(使用ES6语法类的模拟)

Jest可用于模拟导入到要测试的文件中的ES6语法的类。

ES6语法的类是具有一些语法糖的构造函数。因此,ES6语法的类的任何模拟都必须是函数或实际的ES6语法的类(这也是另一个函数)。
所以可以使用模拟函数来模拟它们。如下

跟踪使用情况(监视模拟)(Keeping track of usage (spying on the mock))

注入测试实现很有帮助,但我们可能还想测试是否使用正确的参数调用类构造函数和方法。

监视构造函数(Spying on the constructor)

为了跟踪对构造函数的调用,用一个Jest模拟函数替换HOF返回的函数。
使用jest.fn()创建它,然后使用mockImplementation()指定它的实现。如下

1
2
3
4
5
6
7
8
9
10
import SoundPlayer from '../lib/sound-player';
jest.mock('../lib/sound-player', () => {
// 检查构造函数的调用
return jest.fn().mockImplementation(() => {
return {
choicePlaySoundFile: () => {},
playSoundFile: () => {},
};
});
});

我们使用SoundPlayer.mock.calls来检查我们的模拟类的用法:expect(SoundPlayer).toHaveBeenCalled();
或接近等价的:expect(SoundPlayer.mock.calls.length).toEqual(1);

监视类的方法(Spying on methods of our class)

我们的模拟类需要提供将在测试期间调用的任何成员函数(示例中为playSoundFile),否则我们将因调用不存在的函数而出错。
但是我们可能也希望监视对这些方法的调用,以确保使用预期的参数调用它们。

每次在测试期间调用模拟构造函数时,都会创建一个新对象。
为了监视所有这些对象中的方法调用,我们使用另一个mock函数填充playSoundFile,并在我们的测试文件中存储对同一个mock函数的引用,因此它在测试期间可用。

1
2
3
4
5
6
7
8
9
10
11
import SoundPlayer from '../lib/sound-player';
const mockPlaySoundFile = jest.fn();
const mockChoicePlaySoundFile = jest.fn();
jest.mock('../lib/sound-player', () => {
return jest.fn().mockImplementation(() => {
return {
choicePlaySoundFile: mockChoicePlaySoundFile,
playSoundFile: mockPlaySoundFile,
};
});
});

手动模拟等效于此:

1
2
3
4
5
6
7
8
9
10
11
12
13
export const mockChoicePlaySoundFile = jest.fn();
const mockPlaySoundFile = jest.fn();

const mock = jest.fn().mockImplementation(() => {
const data = {
choicePlaySoundFile: mockChoicePlaySoundFile,
playSoundFile: mockPlaySoundFile,
};

return data;
});

export default mock;

用法类似于模块工厂函数,除了可以省略jest.mock()中的第二个参数,并且必须将模拟方法导入到测试文件中,因为它不再在那里定义。
使用原始模块路径;
不包括__mocks__

在测试之间进行清理(Cleaning up between tests)

要清除对mock构造函数及其方法的调用记录,我们在beforeEach()函数中调用mockClear():

前面的文章分别做了4个实例,分别是下面四个文件,可以自己打开项目去具体看下,这里就不在展示了

1
2
3
4
src/__tests/jest_sound_player.test.js
src/__tests/jest_sound_player_2.test.js
src/__tests/jest_sound_player_3.test.js
src/__tests/jest_sound_player_4.test.js

实践项目地址

1
2
git clone https://github.com/durban89/webpack4-react16-reactrouter-demo.git
git checkout v_1.0.32

项目初始化

1
2
3
4
5
git clone https://github.com/durban89/webpack4-react16-reactrouter-demo.git 
cd webpack4-react16-reactrouter-demo
git fetch origin
git checkout v_1.0.32
npm install

ES6 Class Mocks(使用ES6语法类的模拟)

Jest可用于模拟导入到要测试的文件中的ES6语法的类。

ES6语法的类是具有一些语法糖的构造函数。因此,ES6语法的类的任何模拟都必须是函数或实际的ES6语法的类(这也是另一个函数)。
所以可以使用模拟函数来模拟它们。如下

深入:了解模拟构造函数

使用jest.fn().mockImplementation()构建的构造函数mock,使模拟看起来比实际更复杂。
那么jest是如何创建一个简单的模拟(simple mocks)并演示下mocking是如何起作用的

手动模拟另一个ES6语法的类

如果使用与__mocks__文件夹中的模拟类相同的文件名定义ES6语法的类,则它将用作模拟。
这个类将用于代替真正的类。
我们可以为这个类注入测试实现,但不提供监视调用的方法。如下
src/__mocks__/sound-player.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default class SoundPlayer {
constructor() {
console.log('Mock SoundPlayer: constructor was called');
this.name = 'Player1';
this.fileName = '';
}

choicePlaySoundFile(fileName) {
console.log('Mock SoundPlayer: choicePlaySoundFile was called');
this.fileName = fileName;
}

playSoundFile() {
console.log('Mock SoundPlayer: playSoundFile was called');
console.log('播放的文件是:', this.fileName);
}
}

使用模块工厂参数的简单模拟(Simple mock using module factory parameter)

传递给jest.mock(path,moduleFactory)的模块工厂函数可以是返回函数*的高阶函数(HOF)。
这将允许在模拟上调用new。
同样,这允许为测试注入不同的行为,但不提供监视调用的方法

*模块工厂功能必须返回一个功能(* Module factory function must return a function)

为了模拟构造函数,模块工厂必须返回构造函数。
换句话说,模块工厂必须是返回函数的函数 - 高阶函数(HOF)。如下演示

1
2
3
4
5
6
7
jest.mock('../lib/sound-player', () => {
return function() {
return {
playSoundFile: () => {}
};
};
});

注意:箭头功能不起作用(Note: Arrow functions won’t work)

请注意,mock不能是箭头函数,因为在JavaScript中不允许在箭头函数上调用new。
所以这不起作用:

1
2
3
4
5
6
jest.mock('./sound-player', () => {
return () => {
// 不起作用 箭头函数不会被调用
return {playSoundFile: () => {}};
};
});

这将抛出TypeError:_soundPlayer2.default不是构造函数,除非代码被转换为ES5,例如,
通过babel-preset-env。(ES5没有箭头函数也没有类,因此两者都将被转换为普通函数。)

实践项目地址

1
2
git clone https://github.com/durban89/webpack4-react16-reactrouter-demo.git
git checkout v_1.0.32

项目初始化

1
2
3
4
5
git clone https://github.com/durban89/webpack4-react16-reactrouter-demo.git 
cd webpack4-react16-reactrouter-demo
git fetch origin
git checkout v_1.0.31
npm install

ES6 Class Mocks(使用ES6语法类的模拟)

Jest可用于模拟导入到要测试的文件中的ES6语法的类。

ES6语法的类是具有一些语法糖的构造函数。因此,ES6语法的类的任何模拟都必须是函数或实际的ES6语法的类(这也是另一个函数)。
所以可以使用模拟函数来模拟它们。如下

ES6语法类的实例

这里的实例我使用官方的例子,SoundPlayer类和SoundPlayerConsumer消费者类。下面部分文件的内容参考上篇文章React 16 Jest ES6 Class Mocks(使用ES6语法类的模拟)src/lib/sound-player.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export default class SoundPlayer {
constructor() {
this.name = 'Player1';
this.fileName = '';
}

choicePlaySoundFile(fileName) {
this.fileName = fileName;
}

playSoundFile() {
console.log('播放的文件是:', this.fileName);
}
}

src/lib/sound-player-consumer.js

1
2
3
4
5
6
7
8
9
10
11
12
13
import SoundPlayer from './sound-player';

export default class SoundPlayerConsumer {
constructor() {
this.soundPlayer = new SoundPlayer();
}

play() {
const coolSoundFileName = 'song.mp3';
this.soundPlayer.choicePlaySoundFile(coolSoundFileName);
this.soundPlayer.playSoundFile();
}
}

ES6语法的类测试实例三 - 使用模块工厂参数调用jest.mock()(Calling jest.mock() with the module factory parameter)jest.mock(path,moduleFactory)接受模块工厂参数。

模块工厂是一个返回模拟的函数。
为了模拟构造函数,模块工厂必须返回构造函数。
换句话说,模块工厂必须是返回函数的函数 - 高阶函数(HOF)。测试用例如下
src/__tests__/jest_sound_player_3.test.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import SoundPlayer from '../lib/sound-player';
import SoundPlayerConsumer from '../lib/sound-player-consumer';

jest.mock('../lib/sound-player'); // SoundPlayer 现在是一个模拟构造函数

const mockPlaySoundFile = jest.fn();
const mockChoicePlaySoundFile = jest.fn();

jest.mock('../lib/sound-player', () => jest.fn().mockImplementation(() => ({
choicePlaySoundFile: mockChoicePlaySoundFile,
playSoundFile: mockPlaySoundFile,
})));

beforeEach(() => {
// 清除所有实例并调用构造函数和所有方法:
SoundPlayer.mockClear();
mockChoicePlaySoundFile.mockClear();
});

it('我们可以检查SoundPlayerConsumer是否调用了类构造函数', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
expect(SoundPlayer).toHaveBeenCalledTimes(1);
});

it('我们可以检查SoundPlayerConsumer是否在类实例上调用了一个方法', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
const coolSoundFileName = 'song.mp3';
soundPlayerConsumer.play();
expect(mockChoicePlaySoundFile).toHaveBeenCalledWith(coolSoundFileName);
});

注意上面代码中的这段代码

1
2
3
4
5
6
7
const mockPlaySoundFile = jest.fn();
const mockChoicePlaySoundFile = jest.fn();

jest.mock('../lib/sound-player', () => jest.fn().mockImplementation(() => ({
choicePlaySoundFile: mockChoicePlaySoundFile,
playSoundFile: mockPlaySoundFile,
})));

工厂参数的限制是,由于对jest.mock()的调用被提升到文件的顶部,因此无法首先定义变量然后在工厂中使用它。
对以”mock”开头的变量进行例外处理。

ES6语法的类测试实例四 - 使用mockImplementation()或mockImplementationOnce()替换mock(Replacing the mock using mockImplementation() or mockImplementationOnce())

您可以通过在现有模拟上调用mockImplementation()来替换前面所有的模拟,以便更改单个测试或所有测试的实现。 对jest.mock的调用被提升到代码的顶部。 也可以指定模拟,例如在beforeAll()中,通过在现有mock上调用mockImplementation()或mockImplementationOnce()而不是使用factory参数。 如果需要,这还允许在测试之间更改模拟:测试用例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import SoundPlayer from '../lib/sound-player';
import SoundPlayerConsumer from '../lib/sound-player-consumer';

jest.mock('../lib/sound-player'); // SoundPlayer 现在是一个模拟构造函数

describe('SoundPlayer被调用的时候抛出异常', () => {
beforeAll(() => {
SoundPlayer.mockImplementation(() => ({
playSoundFile: () => {
throw new Error('Test error');
},
choicePlaySoundFile: () => {
throw new Error('Test error');
},
}));
});

it('play被调用的收抛出异常', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
expect(() => soundPlayerConsumer.play()).toThrow();
});
});

上面的代码注意这里

1
2
3
4
5
6
7
8
9
10
beforeAll(() => {
SoundPlayer.mockImplementation(() => ({
playSoundFile: () => {
throw new Error('Test error');
},
choicePlaySoundFile: () => {
throw new Error('Test error');
},
}));
});

实践项目地址

1
2
git clone https://github.com/durban89/webpack4-react16-reactrouter-demo.git
git checkout v_1.0.32

Kibana使用的查询语法是Lucene的查询语法,这里在使用Kibana的同事一定要注意,不然,在进行搜索的时候,你会抓狂的。
下面了解下Lucene的查询语法,了解了Lucene的查询语法也就知道了改如何使用Kibana的使用方式
Lucene查询语法以可读的方式书写,然后使用JavaCC进行词法转换,转换成机器可识别的查询。

词语查询,语法如下

1
2
"here","there"
"here,there"

字段查询,语法如下

1
2
tag:there
tag:"there are"

搜索语句是需要加上双引号,否则

1
tag:there are

就意味着,搜索tag为there,或者包含are关键字的文档

修饰符查询,通过增加修饰,从而扩大查询的范围。

通配符一般包括如下

?:匹配单个字符
*:匹配0个或多个字符

语法如下

1
?tere

意味着搜索there、where等类似的文档

1
test*

意味着搜索test、tests、tester

**模糊词查询,就是在词语后面加上符号~。**语法如下

1
he~

意味着搜索her或hei等词
也可以在~后面添加模糊系数,模糊系数[0-1],越靠近1表示越相近,默认模糊系数为0.5。语法如下

1
he~0.8

邻近词查询,语法如下

1
"here there"~10

代表搜索包含”here”,”there”的文档,这两个词中间可以有一部分内容(这部分的内容通过字符个数显示)
能够匹配到结果的如下

1
2
"here wowo wowo there"
"here,wowow,wowow,there"

范围查询,可以指定最大值和最小值,会自动查找在这之间的文档。如果是单词,则会按照字典顺序搜索。

{}尖括号表示不包含最小值和最大值,可以单独使用
[]方括号表示包含最小值和最大值,可以单独使用。如下:

如果搜索成绩grade字段小于等于80分,大于60分的
可以写成下面的方式

1
grade:{60,80]

如果搜索name在A和C之间的,可以使用如下的语法

1
name:{A,C}

词语相关度查询

如果单词的匹配度很高,一个文档中或者一个字段中可以匹配多次,那么可以提升该词的相关度。使用符号^提高相关度。

提高jarkarta的比重
jakarta apache
可以采用下面的语法:

1
jakarta^4 apache

布尔操作符

支持多种操作符:

AND

AND操作符用于连接两个搜索条件,仅当两个搜索条件都满足时,才认为匹配。通常用来做交集操作。也可以使用&&替换。
注意必须使用大写。如果不使用AND,而是and,可能会被单做关键词进行搜索!

例如:搜索同时包含a和b的文档

1
a AND b

或者

1
a && b

OR

OR操作符用于连接两个搜索条件,当其中一个条件满足时,就认为匹配。通常用来做并集操作。也可以使用||替换。注意必须使用大写。

例如:搜索包含a或者b的文档

1
a OR b

或者

1
a || b

NOT

NOT操作符排除某个搜索条件。通常用来做差集操作也可以使用!替换。注意必须大写。

例如:搜索包含a,不包含b的文档

1
a NOT b

或者

1
a && !b

在kibana中支持单独使用,如:排除包含test的文档

1
NOT test

+(加号)

包含该操作符后跟着的搜索条件,如:搜索包含tom的文档

1
+tom

作用和AND的差不多,但是支持单独使用

-(减号)

排除该操作符后跟着的搜索条件,如:搜索不包含tom的文档

1
-tom

效果类似NOT

分组

支持使用小括号对每个子句进行分组,形成更为复杂的查询逻辑。
例如:要搜索包含a的文档中,也包含b或者c的

1
a AND (b OR c)

也支持在字段中使用小括号。如:要搜索标题中,既包含a也包含b的

1
title:(+a +"b")

转义字符

由于Lucene中支持很多的符号,如

1
+ - && || ! ( ) { } [ ] ^ " ~ * ? : \

因此如果需要搜索 (1+1):2 需要对改串进行转换,使用字符\。

1
\(1\+1\)\:2

参考文档:
http://www.cnblogs.com/xing901022/p/4974977.html
https://segmentfault.com/a/1190000002972420

项目初始化

1
2
3
4
5
git clone https://github.com/durban89/webpack4-react16-reactrouter-demo.git 
cd webpack4-react16-reactrouter-demo
git fetch origin
git checkout v_1.0.30
npm install

ES6 Class Mocks(使用ES6语法类的模拟)

Jest可用于模拟导入到要测试的文件中的ES6语法的类。

ES6语法的类是具有一些语法糖的构造函数。因此,ES6语法的类的任何模拟都必须是函数或实际的ES6语法的类(这也是另一个函数)。
所以可以使用模拟函数来模拟它们。如下

ES6语法的类测试实例二,今天使用第二种方式 - 手动模拟(Manual mock)

ES6语法类的实例

这里的实例我使用官方的例子,SoundPlayer类和SoundPlayerConsumer消费者类。下面部分文件的内容参考上篇文章React 16 Jest ES6 Class Mocks(使用ES6语法类的模拟)src/lib/sound-player.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export default class SoundPlayer {
constructor() {
this.name = 'Player1';
this.fileName = '';
}

choicePlaySoundFile(fileName) {
this.fileName = fileName;
}

playSoundFile() {
console.log('播放的文件是:', this.fileName);
}
}

src/lib/sound-player-consumer.js

1
2
3
4
5
6
7
8
9
10
11
12
13
import SoundPlayer from './sound-player';

export default class SoundPlayerConsumer {
constructor() {
this.soundPlayer = new SoundPlayer();
}

play() {
const coolSoundFileName = 'song.mp3';
this.soundPlayer.choicePlaySoundFile(coolSoundFileName);
this.soundPlayer.playSoundFile();
}
}

通过在__mocks__文件夹中创建一个模拟实现来创建手动模拟。
这个可以指定实现,并且可以通过测试文件使用它。如下
src/lib/mocks/sound-player.js

1
2
3
4
5
6
7
8
9
10
11
12
13
export const mockChoicePlaySoundFile = jest.fn();
const mockPlaySoundFile = jest.fn();

const mock = jest.fn().mockImplementation(() => {
const data = {
choicePlaySoundFile: mockChoicePlaySoundFile,
playSoundFile: mockPlaySoundFile,
};

return data;
});

export default mock;

然后在测试用例中导入mock和mock方法,具体如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import SoundPlayer, { mockChoicePlaySoundFile } from '../lib/sound-player';
import SoundPlayerConsumer from '../lib/sound-player-consumer';

jest.mock('../lib/sound-player'); // SoundPlayer 现在是一个模拟构造函数

beforeEach(() => {
// 清除所有实例并调用构造函数和所有方法:
SoundPlayer.mockClear();
mockChoicePlaySoundFile.mockClear();
});

it('我们可以检查SoundPlayerConsumer是否调用了类构造函数', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
expect(SoundPlayer).toHaveBeenCalledTimes(1);
});

it('我们可以检查SoundPlayerConsumer是否在类实例上调用了一个方法', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
const coolSoundFileName = 'song.mp3';
soundPlayerConsumer.play();
expect(mockChoicePlaySoundFile).toHaveBeenCalledWith(coolSoundFileName);
});

运行后得到的结果如下

1
2
3
4
5
6
7
8
9
 PASS  src/__tests__/jest_sound_player_2.test.js
✓ 我们可以检查SoundPlayerConsumer是否调用了类构造函数 (7ms)
✓ 我们可以检查SoundPlayerConsumer是否在类实例上调用了一个方法 (2ms)

Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 3.352s
Ran all test suites matching /src\/__tests__\/jest_sound_player_2.test.js/i.

下次介绍第三、四种方法 - 使用模块工厂参数调用jest.mock()(Calling jest.mock() with the module factory parameter)和使用mockImplementation()或mockImplementationOnce()替换mock(Replacing the mock using mockImplementation() or mockImplementationOnce())

实践项目地址

1
2
git clone https://github.com/durban89/webpack4-react16-reactrouter-demo.git
git checkout v_1.0.31

项目初始化

1
2
3
4
5
git clone https://github.com/durban89/webpack4-react16-reactrouter-demo.git 
cd webpack4-react16-reactrouter-demo
git fetch origin
git checkout v_1.0.29
npm install

ES6 Class Mocks(使用ES6语法类的模拟)

Jest可用于模拟导入到要测试的文件中的ES6语法的类。

ES6语法的类是具有一些语法糖的构造函数。因此,ES6语法的类的任何模拟都必须是函数或实际的ES6语法的类(这也是另一个函数)。
所以可以使用模拟函数来模拟它们。如下

一个ES6语法类的实例

这里的实例我使用官方的例子,SoundPlayer类和SoundPlayerConsumer消费者类。

src/lib/sound-player.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export default class SoundPlayer {
constructor() {
this.name = 'Player1';
this.fileName = '';
}

choicePlaySoundFile(fileName) {
this.fileName = fileName;
}

playSoundFile() {
console.log('播放的文件是:', this.fileName);
}
}

src/lib/sound-player-consumer.js

1
2
3
4
5
6
7
8
9
10
11
12
13
import SoundPlayer from './sound-player';

export default class SoundPlayerConsumer {
constructor() {
this.soundPlayer = new SoundPlayer();
}

play() {
const coolSoundFileName = 'song.mp3';
this.soundPlayer.choicePlaySoundFile(coolSoundFileName);
this.soundPlayer.playSoundFile();
}
}

这个测试官方介绍了四种方式来创建一个ES6语法的类测试,今天先使用第一种方式 - 自动模拟(Automatic mock)

调用jest.mock('../lib/sound-player')会返回一个有用的“自动模拟”,可以使用它来监视对类构造函数及其所有方法的调用。
它用模拟构造函数替换ES6语法的类,并使用总是返回undefined的mock函数替换它的所有方法。
方法调用保存在AutomaticMock.mock.instances [index] .methodName.mock.calls中。
请注意,如果在类中使用箭头功能,它们将不会成为模拟的一部分。
原因是箭头函数不存在于对象的原型上,它们只是包含对函数的引用的属性。
如果不需要替换类的实现,这是最简单的设置选项。测试用例如下:

src/__tests__/jest_sound_player.test.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import SoundPlayer from '../lib/sound-player';
import SoundPlayerConsumer from '../lib/sound-player-consumer';

jest.mock('../lib/sound-player'); // SoundPlayer 现在是一个模拟构造函数

beforeEach(() => {
// 清除所有实例并调用构造函数和所有方法:
SoundPlayer.mockClear();
});

it('我们可以检查SoundPlayerConsumer是否调用了类构造函数', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
expect(SoundPlayer).toHaveBeenCalledTimes(1);
});

it('我们可以检查SoundPlayerConsumer是否在类实例上调用了一个方法', () => {
// 检查 mockClear() 会否起作用:
expect(SoundPlayer).not.toHaveBeenCalled();

const soundPlayerConsumer = new SoundPlayerConsumer();
// 类构造函数再次被调用
expect(SoundPlayer).toHaveBeenCalledTimes(1);

const coolSoundFileName = 'song.mp3';
soundPlayerConsumer.play();

// mock.instances可用于自动模拟
const mockSoundPlayerInstance = SoundPlayer.mock.instances[0];
const mockChoicePlaySoundFile = mockSoundPlayerInstance.choicePlaySoundFile;
expect(mockChoicePlaySoundFile.mock.calls[0][0]).toEqual(coolSoundFileName);
// 相当于上面的检查
expect(mockChoicePlaySoundFile).toHaveBeenCalledWith(coolSoundFileName);
expect(mockChoicePlaySoundFile).toHaveBeenCalledTimes(1);
});

运行会得到类似如下输出

1
2
3
4
5
6
7
8
9
 PASS  src/__tests__/jest_sound_player.test.js
✓ 我们可以检查SoundPlayerConsumer是否调用了类构造函数 (4ms)
✓ 我们可以检查SoundPlayerConsumer是否在类实例上调用了一个方法 (3ms)

Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 2.27s
Ran all test suites matching /src\/__tests__\/jest_sound_player.test.js/i.

下次介绍第二种方法 - 手动模拟(Manual mock)

实践项目地址

1
2
git clone https://github.com/durban89/webpack4-react16-reactrouter-demo.git
git checkout v_1.0.30

项目初始化

1
2
3
4
5
git clone https://github.com/durban89/webpack4-react16-reactrouter-demo.git 
cd webpack4-react16-reactrouter-demo
git fetch origin
git checkout v_1.0.28
npm install

Using with ES module imports(使用ES模块导入)

我们在使用ES模块时做导入的时候,会习惯性的将导入的语句放在测试文件的顶部。但是在使用Jest的时候,需要指示Jest在模块使用之前进行模拟操作。
正是由于这个原因,Jest会自动将jest.mock的调用提升到模块的顶部(在任何导入之前)

Mocking methods which are not implemented in JSDOM(模拟JSDOM中未实现的方法)

如果某些代码在使用JSDOM(Jest使用的DOM实现)尚未实现的方法的时候,这个情况在测试时是比较麻烦的。比如我们调用window.matchMedia()的时候,Jest返回TypeError:window.matchMedia不是函数,并且没有正确执行测试。
在这种情况下,在测试文件中模拟matchMedia就可以解决类似的问题:如下

1
2
3
4
5
6
7
8
9
window.matchMedia = jest.fn().mockImplementation(query => {
return {
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
};
});

如果window.matchMedia()用于在测试中调用的函数(或方法),则此方法有效。
如果window.matchMedia()直接在测试文件中执行,则Jest报告相同的错误。
在这种情况下,解决方案是将手动模拟移动到单独的文件中,并在测试文件之前将其包含在测试中:实例如下

1
2
3
4
5
6
import './matchMedia.mock'; // Must be imported before the tested file
import { myMethod } from './file-to-test';

describe('myMethod()', () => {
// Test the method here...
});

这里官方就是简单的介绍了下,导致有些学者可能还是云里雾里的,下面简单的写个实例
创建文件
src/lib/matchMedia.mock.js

1
2
3
4
5
6
7
8
9
10
11
window.matchMedia = jest.fn().mockImplementation((query) => {
const obj = {
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
};

return obj;
});

src/lib/useMatchMedia.js

1
2
3
4
5
6
7
8
const useMatchMedia = () => {
const res = window.matchMedia;
return res;
};

module.exports = {
useMatchMedia,
};

创建完文件后,添加测试文件
src/__tests__/useMatchMedia.test.js
第一次我们不调用src/lib/matchMedia.mock.js这个文件

1
2
3
4
5
6
7
8
9
10
// import '../lib/matchMedia.mock';
import { useMatchMedia } from '../lib/useMatchMedia';

describe('useMatchMedia()', () => {
it('useMatchMedia() 被调用', () => {
const res = useMatchMedia();
expect(res).toBeUndefined();
// expect(res).toBeDefined();
});
});

第二次我们调用src/lib/matchMedia.mock.js这个文件

1
2
3
4
5
6
7
8
9
import '../lib/matchMedia.mock';
import { useMatchMedia } from '../lib/useMatchMedia';

describe('useMatchMedia()', () => {
it('useMatchMedia() 被调用', () => {
const res = useMatchMedia();
expect(res).toBeDefined();
});
});

从上面的例子大概就能理解基本上官方的意思了,具体如何使用,这个可以自己有发挥。

如果国内的用户还是不知道如何流畅的运行jest的话建议翻翻我前面的文章。
项目实践地址

1
2
https://github.com/durban89/webpack4-react16-reactrouter-demo.git
tag:v_1.0.29

项目初始化

1
2
3
4
5
git clone https://github.com/durban89/webpack4-react16-reactrouter-demo.git 
cd webpack4-react16-reactrouter-demo
git fetch origin
git checkout v_1.0.27
npm install

手动模拟(Manual Mocks)

手动模拟主要功能是用于存储模拟的数据。

例如,我可能希望创建一个允许您使用虚假数据的手动模拟,而不是访问网站或数据库等远程资源。

这可以确保您的测试快速且不易碎(not flaky)。

模拟水果模块(Mocking fruit modules)

通过在紧邻模块的__mocks__/子目录中编写模块来定义手动模拟。这个方式我在前面文章中的实例中也有用到过,具体的可以参考之前的文章,这里我说下大概的流程

例如,要在src/lib目录中模拟一个名为fruit的模块,则分别创建文件src/lib/fruit.js和文件src/lib/__mocks__/fruit.js的文件。

请注意__mocks__文件夹区分大小写。如果命名目录是__MOCKS__,则可能在某些系统上测试的时候会中断。

注意点

当我们在测试中需要该模块时,还需要显式的调用jest.mock(‘./moduleName’)。

模拟Node核心模块(Mocking Node modules)

如果正在模拟的模块是Node module(例如:lodash),则模拟应放在与node_modules相邻的__mocks__目录中(除非您将根配置为指向项目根目录以外的文件夹)并将自动模拟。

没有必要显式调用jest.mock(‘module_name’)。

可以通过在与范围模块的名称匹配的目录结构中创建文件来模拟范围模块。

例如,要模拟名为@scope/project-name的作用域模块,请在__mocks__/@scope/project-name.js创建一个文件,相应地创建@scope/目录。

注意点

如果我们想模拟Node的核心模块(例如:fs或path),那么明确地调用。

例如:jest.mock(‘path’)是必需的,因为默认情况下不会模拟核心Node模块。

实例演示

当给定模块存在手动模拟时,Jest的模块系统将在显式调用jest.mock(‘moduleName’)时使用该模块。

但是,当automock设置为true时,即使未调用jest.mock(‘moduleName’),也将使用手动模拟实现而不是自动创建的模拟。

要选择不使用此行为,您需要在应使用实际模块实现的测试中显式调用jest.unmock(‘moduleName’)。

注意点

为了正确模拟,Jest需要jest.mock(‘moduleName’)与require/import语句在同一范围内。

假设我们有一个模块,它提供给定目录中所有文件的摘要。在这种情况下,我们使用核心(内置)fs模块来演示

src/lib/FileSummarizer.js

1
2
3
4
5
6
7
8
9
10
const fs = require('fs');

function summarizeFilesInDirectorySync(directory) {
return fs.readdirSync(directory).map(fileName => ({
directory,
fileName,
}));
}

exports.summarizeFilesInDirectorySync = summarizeFilesInDirectorySync;

由于我们希望我们的测试避免实际操作磁盘(这非常慢且易碎[fragile]),我们通过扩展自动模拟为fs模块创建手动模拟。

我们的手动模拟将实现我们可以为我们的测试构建的fs API的自定义版本:

src/lib/__mocks__/fs.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const path = require('path');

const fs = jest.genMockFromModule('fs');

let mockFiles = Object.create(null);

function __setMockFiles(newMockFiles) {
mockFiles = Object.create(null);

const keys = Object.keys(newMockFiles);

for (let index = 0; index < keys.length; index += 1) {
const file = keys[index];
const dir = path.dirname(file);
if (!mockFiles[dir]) {
mockFiles[dir] = [];
}
mockFiles[dir].push(path.basename(file));
}
}

function readdirSync(directoryPath) {
return mockFiles[directoryPath] || [];
}

fs.__setMockFiles = __setMockFiles;
fs.readdirSync = readdirSync;

module.exports = fs;

现在我们编写测试。

请注意,我们需要明确告诉我们要模拟fs模块,因为它是一个核心Node模块:

src/__tests__/FileSummarizer-test.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const fs = require('fs');
const FileSummarizer = require('../lib/FileSummarizer');

jest.mock('fs');

describe('listFilesInDirectorySync', () => {
const MOCK_FILE_INFO = {
'/path/to/file1.js': 'console.log("file1 contents");',
'/path/to/file2.txt': 'file2 contents',
};

beforeEach(() => {
// Set up some mocked out file info before each test
fs.__setMockFiles(MOCK_FILE_INFO);
});

test('includes all files in the directory in the summary', () => {
const fileSummary = FileSummarizer.summarizeFilesInDirectorySync('/path/to');

expect(fileSummary.length).toBe(2);
});
});

此处显示的示例模拟使用jest.genMockFromModule生成自动模拟,并覆盖其默认行为。

这是推荐的方法,但完全是可选的。

如果您根本不想使用自动模拟,则只需从模拟文件中导出自己的函数即可。

完全手动模拟的一个缺点是它们是手动的 - 这意味着你必须在它们模拟的模块发生变化时手动更新它们。

因此,最好在满足您的需求时使用或扩展自动模拟。

为了确保手动模拟及其实际实现保持同步,在手动模拟中使用require.requireActual(moduleName)并在导出之前使用模拟函数修改它可能是有用的。

项目实践地址

1
2
https://github.com/durban89/webpack4-react16-reactrouter-demo.git
tag:v_1.0.28

项目初始化

1
2
3
4
5
git clone https://github.com/durban89/webpack4-react16-reactrouter-demo.git 
cd webpack4-react16-reactrouter-demo
git fetch origin
git checkout v_1.0.26
npm install

定时器模拟(Timer Mocks)

原生定时器功能(即setTimeout,setInterval,clearTimeout,clearInterval)对于测试环境来说不太理想,因为它们依赖于实时时间。
Jest可以将定时器换成允许我们自己控制时间的功能。
示例如下
src/lib/timerGame.js

1
2
3
4
5
6
7
8
9
function timerGame(callback) {
console.log('Ready....go!');
setTimeout(() => {
console.log('Times up -- stop!');
return callback && callback();
}, 1000);
}

module.exports = timerGame;

src/__tests__/jest_timerGame.test.js

1
2
3
4
5
6
7
8
9
10
const timerGame = require('../lib/timerGame');

jest.useFakeTimers();

test('等待1秒钟后结束游戏', () => {
timerGame();

expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);
});

这里我们通过调用jest.useFakeTimers();来启用假定时器。
这使用模拟函数模拟了setTimeout和其他计时器函数。
如果在一个文件或描述块中运行多个测试,则jest.useFakeTimers();
可以在每次测试之前手动调用,也可以使用诸如beforeEach之类的设置函数调用。
不这样做会导致内部使用计数器不被重置。

运行所有计时器(Run All Timers)

为上面的模块timerGame写一个测试,这个测试在1秒钟后调用回调callback,示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const timerGame = require('../lib/timerGame');
jest.useFakeTimers();

test('1秒钟后调用回调callback', () => {
const callback = jest.fn();

timerGame(callback);

// 在这个时间点上,callback回调函数还没有被调用
expect(callback).not.toBeCalled();

// 所有timers被执行
jest.runAllTimers();

// 现在我们的callback回调函数被调用
expect(callback).toBeCalled();
expect(callback).toHaveBeenCalledTimes(1);
});

运行待定时间器

在某些情况下,您可能还有一个递归计时器 - 这是一个在自己的回调中设置新计时器的计时器。
对于这些,运行所有计时器将是一个无限循环,所以像jest.runAllTimers()这样的东西是不可取的。
对于这些情况,您可以使用jest.runOnlyPendingTimers()。示例如下
src/lib/infiniteTimerGame.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function infiniteTimerGame(callback) {
console.log('Ready....go!');

setTimeout(() => {
console.log('Times up! 10 seconds before the next game starts...');

if (callback) {
callback();
}

// 10秒钟后执行下一个
setTimeout(() => {
infiniteTimerGame(callback);
}, 10000);
}, 1000);
}

module.exports = infiniteTimerGame;

src/__tests__/jest_infiniteTimerGame.test.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const infiniteTimerGame = require('../lib/infiniteTimerGame');

jest.useFakeTimers();

describe('infiniteTimerGame', () => {
test('schedules a 10-second timer after 1 second', () => {
const callback = jest.fn();

infiniteTimerGame(callback);

// 在这里,会在意秒钟后执行callback的回调
expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);

// 只有当前待定的计时器(但不是在该过程中创建的任何新计时器)
jest.runOnlyPendingTimers();

// 此时,1秒钟的计时器应该已经被回调了
expect(callback).toBeCalled();

// 它应该创建一个新的计时器,以便在10秒内启动游戏
expect(setTimeout).toHaveBeenCalledTimes(2);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 10000);
});
});

按时间提前计时器(Advance Timers by Time)

另一种可能性是使用jest.advanceTimersByTime(msToRun)。
调用此API时,所有计时器都按msToRun毫秒提前。
将执行已经通过setTimeout()或setInterval()排队并且将在此时间帧期间执行所有待处理”宏任务”。
此外,如果这些宏任务调度将在同一时间帧内执行的新宏任务,那么将执行这些宏任务,直到队列中不再有宏任务应该在msToRun毫秒内运行。
示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const timerGame = require('../lib/timerGame');

jest.useFakeTimers();

it('1秒钟后通过advanceTimersByTime调用回调函数', () => {
const callback = jest.fn();

timerGame(callback);

// callback还没有被执行
expect(callback).not.toBeCalled();

// 提前1秒钟执行
jest.advanceTimersByTime(1000);

// 所有的callback被调用
expect(callback).toBeCalled();
expect(callback).toHaveBeenCalledTimes(1);
});

在某些测试中偶尔可能有用,就是在测试中可以清除所有挂起的计时器。
为此,可以使用jest.clearAllTimers()。

项目实践地址

1
2
https://github.com/durban89/webpack4-react16-reactrouter-demo.git
tag:v_1.0.27
0%