Jest 是个开源的基于Jasmine框架的JavaScrit 单元测试框架。

Jest的目标是减少开始测试一个项目所要花费的是件和认知负荷。

  • Jeff Morrison

Jest的特点,易于组织,快速响应,测试快照。

Hello world

安装 Jest

1
npm i jest -D

创建 hw.js文件 文件内容如下:

1
2
3
4
function hw () {
return 'Hello World!'
};
module.exports = hw;

创建测试文件 hw.test.js:

1
2
3
4
const hw = require('./hw');
test('test hw function',()=>{
expect(hw()).toBe("Hello World!"); //执行hw 方法,将返回值于 toBe中的数值做对比
})

Jest默认会查找 项目文件中后缀为.test.js的文件,进行执行。
项目中我们把测试文件 写在 __tests__ 文件夹下。

Jest 与 Babel,Webpack 一起使用

使用Babel需安装 babel-jest

1
npm i babel-preset-env babel-jest -D

如果你没有使用Yarn 或者Npm版本低于3,你还需要安装 regenerator-runtime

1
npm i regenerator-runtime -D

最后需要配置.babelrc文件

1
2
3
{
"preset":["env"]
}

当我们用Webpack构建项目时,我们经常将css,字体引入到js文件中,然后通过Webpack打包对这些静态资源进行处理。
Jest 无法解析这些静态资源,需要通过Jest配置Mock这些静态资源。
Jest 配置如下: package.json

1
2
3
4
5
6
7
{
"jest":{
"moduleNameMapper":{
"\\.css$":"<rootDir>/__mock__/styleMock.js"
}
}
}
1
export default {};

Jest 与 TypeScript

安装相关依赖包

1
npm i ts-jest @types/jest typescript -D

配置 package.json

1
2
3
4
5
6
7
8
9
{
"jest":{
"transform":{
".(ts|tsx)":"<rootDir>/node_modules/ts-jest/preprocessor.js",

},
"testRegex":"(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$",
"moduleFileExtensions":["ts","tsx","js","json"]
}

Jest全局方法

Describe(name, fn)

测试套件,一组相关的测试用例。第一个参数是测试套餐的描述,第二个参数是测试用例

1
2
3
4
5
6
7
8
9
10
11
12
13
const my = {
name : "fynn",
age : 27
}
describe("my info", ()=>{
test("my name", ()=>{
expect(my.name).toBe("fynn")
});
test("my age", ()=>{
expect(my.age).toBe(27)
})

})

Describe.only(name, fn)

当一个file有多个测试套件,但你只想执行其中一个测试套件,可以使用 describe.only

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const my = {
name : "fynn",
age : 27
}
let hw = () =>"Hello World!";
describe("my info", ()=>{
test("my name", ()=>{
expect(my.name).toBe("fynn")
});
test("my age", ()=>{
expect(my.age).toBe(27)
})

});
describe.only('hw function test suit',()=>{
test('hw test',()=>{
expect(hw()).toBe("Hello World!");
})
})

Describe.skip(name, fn)

一个file中有多个测试套件,如果你想跳过某个测试套件可以使用 describe.skip

Test

测试用例,可以写在 describe测试套件中,也可以单独写在测试套件外面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const my = {
name : "fynn",
age: 27
}
let hw = ()=>"Hello World!"
describe("my info",()=>{
test("my name",()=>{
expect(my.name).toBe("fynn")
});
test("my age",()=>{
expect(my.age).toBe(27)
})
});
test("hw test",()=>{
expect(hw()).toBe("Hello World!");
})

Test.only

有多个测试用例或测试套件,只想执行其中某一个测试用例时可以用test.only。

1
2
3
4
5
6
7
8
9
10
11
const my = {
name : "fynn",
age : 27
};
let hw = ()=>"Hello World!";
test("my name",()=>{
expect(my.name).toBe("fynn");
})
test.only("hw test",()=>{
expect(hw()).toBe("Hello World!");
})

Test.skip(name, fn)

当有多个测试用例,想跳过某个测试用例可以使用test.skip;

It(name,fn)

用法和test一样,不能嵌套在test中!可以嵌套在describe中

1
2
3
4
5
6
7
8
9
10
11
const my = {
name : "fynn",
age : 27
};
let hw = ()=>"Hello World!";
it("my name",()=>{
expect(my.name).toBe("fynn");
})
xit("hw test",()=>{
expect(hw()).toBe("Hello World!");
})

xit 类似与test.skip,跳过这个测试实例。

AfterAll(fn)

当file所有test都执行完毕后,执行afterAll中的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
const my = {
name :"fynn",
age : 27
};
test("my name",()=>{
expect(my.name).toBe("fynn")
});
test("my age",()=>{
expect(my.age).toBe(27)
});
afterAll(()=>{
console.log("执行完所有test!")
})

AfterEach(fn)

每当一个test执行完后,调用一次afterEach中的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
const my = {
name :"fynn",
age : 27
};
test("my name",()=>{
expect(my.name).toBe("fynn")
});
test("my age",()=>{
expect(my.age).toBe(27)
});
afterEach(()=>{
console.log("执行完一个test!")
})

BeforeAll(fn)

在所有执行test前先调用beforeAll中的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
const my = {
name :"fynn",
age : 27
};
test("my name",()=>{
expect(my.name).toBe("fynn")
});
test("my age",()=>{
expect(my.age).toBe(27)
});
beforeAll(()=>{
console.log("要开始执行test了!")
});

BeforeEach(fn)

在每个test执行前都会调用一次beforeEach中的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
const my = {
name :"fynn",
age : 27
};
test("my name",()=>{
expect(my.name).toBe("fynn")
});
test("my age",()=>{
expect(my.age).toBe(27)
});
beforeEach(()=>{
console.log("要开始执行一个test了!")
})

使用适配器

Jest提供了很多适配器用于比较不同类型的测试值是否正确。

通用的Matchers

1
2
3
test("two plus two is four", ()=>{
expect(2+2).toBe(4)
})

toBe 用来匹配expect值类型number值或string值的返回值。
toBe 使用 ‘===’测试值是否相等。

1
2
3
4
5
6
7
8
test('Object assignment', ()=>{
const data = {one:1};
data["two"] = 2;
expect(data).toEqual({
one: 1,
two: 2
})
})

toEqual 用于比较每个数组或对象中每个字段。
toEqual 字段值的比较也是用“===”。

null、undefined、boolean匹配

有时候需要测试我们的值是否是 null、undefined、false、true等特殊值。Jest提供这些值的匹配方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
toBeNull --- 只匹配 null
toBeUndefined --- 只匹配 undefined
toBeDefined --- 匹配 非undefined
toBeTruthy --- 匹配所有 if状态是 true
toBeFalsy --- 匹配所有if状态是false

test("null,undefined,boolean",()=>{
const n = null;
const u = undefined;
const t = 'str';
const f = 0;
expect(n).toBeNull();
expect(u).toBeUndefined();
expect(t).toBeTruthy();
expect(n).toBeFalsy();
})

Numbers

Jest提供了数值大小的匹配器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
toBeGreaterThan --- 大于
toBeGreaterThanOrEqual --- 大于或等于
toBeLessThan --- 小于
toBeLessThanOrEqual --- 小于或等于
toBe --- 等于
toEqual --- 等于

test("one plus two",()=>{
const value = 1+2;
expect(value).toBeGreaterThan(2.5);
expect(value).toBeGreaterThanOrEqual(3);
expect(value).toBeLessThan(3.5);
expect(value).toBeLessThanOrEqual(3);

expect(value).toBe(3)
expect(value).toEqual(3);
})

进行浮点型计算时,使用__toBeCloseTo__ 进行值的比较

1
2
3
4
5
6
test("adding floating point numnber",()=>{
cosnt value = 0.1+0.2;
expect(value).toBeCloseTo(0.3); //passed
expect(value).toBe(0.3) // failed
expect(value).not.toBe(0.3) // passed
})

Stings

字符串正则表达式匹配器使用__ToMatch__

1
2
3
4
5
6
test("there is no I in team",()=>{
expect('team').not.toMatch(/I/);
})
test('but there is a "stop" in Christoph', ()=>{
expect("Christoph").toMatch(/stop/)
})

匹配字符串长度使用__toHaveLength__

1
2
3
test('"abc" is length',()=>{
expect("abc").toHaveLength(3);
})

toHaveLength 也可以匹配object中有 .length属性的值。

1
2
3
test("arr is length",()=>{
expect([1,2,3]).toHaveLength(3)
})

Arrays

匹配数组中某个值使用__toContain__

1
2
3
4
const shoppingList=["diapers","kleenex","trash bags","paper towels","beer"];
test("the shopping list has beer on it",()=>{
expect(shoppingList).toContain('beer');
})

匹配某个数组是否是另一个数组的子集使用__expect.arrayContaining(array)

1
2
3
4
test("arrayContaining",()=>{
const expected = [2,3];
expect([1,2,3,4,5,6]).toEqual(expect.arrayContaining(expected))
})

异步测试

实际开发中我们经常会进行异步操作,Jest提供了三种进行异步操作测试的方式。

Callbacks

在callback方式中的测试实例第二个执行函数会有个参数__done__,只有当done被调用这个测试实例才算完结。

1
2
3
4
5
6
7
8
test('the data is peanut butter',done=>{
function callback(data) {
expect(data).toBe('peanut butter');
done();
}

fetchData(callback)
})

如果done() 未被调用,这个测试实例就算失败。

Promises

如果你使用promises,在你的测试实例回调函数返回一个promise,Jest会等待这个promise状态变为resolve。如果promise状态变为rejected,这个测试实例会自动变成fail。

1
2
3
4
5
test('the data is peanut butter',()=>{
return fetchData().then(data=>{
expect(data).toBe('peanut butter');
})
})

也可以直接用__resolves__将放回结果进行验证。

1
2
3
test('the data is peanut butter',()=>{
return expect(fetchData()).resolves.toBe('peanut butter');
})

Async/Await

使用async 定义测试实例回掉函数,用await等待异步执行。

1
2
3
test('the data is peanut butter',async ()=>{
await expect(fetchData()).resolves.toBe('peanut butter');
})

Mock 函数

Mock函数可以记录下一个函数执行的信息(参数,实例对象)。

创建Mock函数

1
const mockFn = jest.fn();

mockFn 如果没有传递实例函数,它默认返回 undefined

mockFn.mock.calls

返回MockFn实例对象,实例化传递过的参数,以数组方式。

1
2
3
4
5
const mockFn = jest.fn((a,b)=>`${a} --- ${b}`);
var f1 = new mockFn('f1-arga','f1-argb')
var f2 = new mockFn('f2-arga','f2-argb')
var calls = mockFn.mock.calls();
// calls : [['f1-arga','f1-argb'],['f2-arga','f2-argb']]

mockFn.mock.instances

返回mockFn 实例过的对象,以数组方式。

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

const a = new mockFn();
const b = new mockFn();

mockFn.mock.instances[0] === a; // true
mockFn.mock.instances[1] === b; // true

mockFn.mockImplementation(fn)

接受一个函数,它会做为mock的实例方法。这个mock还是可以记录对他调用所有使用的参数。

1
2
3
4
5
6
7
8
9
const mockFn = jest.fn().mockImplementation(scalar=>42+scalar);
// or: jest.fn(scalar=>42+scalar);
const a = mockFn(0);
const b = mockFn(1);

a === 42 // true
b === 43 // true
mockFn.mock.calls[0][0] === 0; // true
mockFn.mock.calls[1][0] === 1; // true

mockImplementation 也可以用于类的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
//someClass.js
export class SomeClass { m (a,b) {}}

//OtherModule.test.js
import SomeClass from './someClass'
const mMock = jest.fn();
SomeClass.mockImplementation(()=>{
return {m:mMock}
})

const some = someClass();
some.m('a','b');
console.log('Calls to m:', mMock.mock.calls)

mockImplementation 一般用户自定义个月mock实例,替换调引用进来的原实例方法

1
2
3
4
5
6
7
8
// foo.js
export function foo (){return 40};

//test.js
import foo from './foo'
jest.mock('./foo')
foo.mockImplementation(()=>42);
foo(); // 42

mockFn.mockImplementationOnce(fn)

通过 mockImplementationOnce mock可以接受多个实例方法。当调用mock时,依次调用定义好的实例方法,当调用次数超过 mockImplementationOnce 次数,则返回默认实例方法值,如果没有定义默认实例方法则返回undefined

1
2
3
4
5
6
var myMockFn = jest.fn()
.mockImplementationOnce(cb=>cb(null,true))
.mockImplementationOnce(cb=>cb(null,false))

myMockFn((err, val)=>console.log(val)); //true
myMockFn((err, val)=>console.log(val)); //false

Manual Mocks

在开发时我们需要调用后台数据,可以通过创建manual mock模拟后台数据,这样使得测试更容易更快。
Manual Mocks模块应写在文件夹__mocks__下,__mocks__ 文件夹应该和被mock的模块放在一起

例子

1
2
3
4
5
6
7
8
9
10
.
├── config
├── __mocks__
│ └── fs.js
├── models
│ ├── __mocks__
│ │ └── user.js
│ └── user.js
├── node_modules
└── views

__mocks__/user.js mock user.js
例子源码

Jest 配置项

collectCoverage

collectCoverage用来设置是否启用测试覆盖报告. 如果启用则会增加执行单元测试用例时间.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module.exports = {
preset: "@vue/cli-plugin-unit-jest/presets/typescript-and-babel",
testMatch: ["**/test/**/*.spec.[jt]s?(x)"],
moduleFileExtensions: ["js", "ts", "vue"],
moduleNameMapper: {
"^@ModelApp/(.*)$": "<rootDir>/src/$1"
},
collectCoverage: true,
collectCoverageFrom: [
"./src/store/**/*.{js,ts}",
"./src/modules/**/*.{js,ts}",
"./src/utils/**/*.{js,ts}",
"./src/common/**/*.{js,ts}",
"!**/node_modules/**"
]
}
  • stmts(Statements)是语句覆盖率(statement coverage):是不是每个语句都执行了?

  • Branch(Branches)分支覆盖率(branch coverage):是不是每个if代码块都执行了?

  • Funcs(Functions)函数覆盖率(function coverage):是不是每个函数都调用了?

  • Lines行覆盖率(line coverage):是不是每一行都执行了?

collectCoverageFrom

collectCoverageFrom用来设置 单元测试报告覆盖的 文件目录.

1
2
3
4
5
6
7
{
"collectCoverageFrom": [
"**/*.{js,jsx}",
"!**/node_modules/**",
"!**/vendor/**"
]
}

参考:

https://facebook.github.io/jest/docs/getting-started.html