单元测试之文件模块
如何测试
步骤
- 选择测试工具: jest
- 设计测试用例
- 写测试,运行测试,改代码
- 单元测试、功能测试、集成测试
重点
- 单元测试不应该与外界打交道(那是集成测试要做的)
- 单元测试的对象是函数(eg:
db.read()
和db.whrite()
) - 功能测试的对象是模块(同时测试很多函数)
- 集成测试的对象是系统
- 我们只学习单元测试
Jest文档
查看文档的时候一定要使用package.json
中对用的jest的版本,否则用旧版本的jest,对照新版本的文档,就会出现意料之外的结果(项目中用到的是24.9
版本)
安装
查看jest的官方文档,看文档就是用CRM学习法,copy能运行的代码
安装下jest,jest一般属于开发者的依赖,因此要加--dev
npm install --save-dev jest
尝试
1.创建 sum.js
文件︰
function sum(a, b) {
return a + b;
}
module.exports = sum;
2.创建名为 sum.test.js
的文件
const sum = require('./sum');
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
3.将下列配置内容添加到您的 package.json
:
{
"scripts": {
"test": "jest"
}
}
4.最后,运行 yarn test
或 npm run test
,Jest将自动的去寻找以test.js
结尾的文件
测试通过,一加二等于三
5.copy完代码,现在可以改一改代码再运行看看
webstorm中对jest的代码不提示且有波浪线警告,我们只需要下载jest Library 即可
点击DOWNLOAD,点击任意地方,搜索jest,点击download and install 即可
我们可以点击左侧的播放按键,运行测试用例
如果测试通过,就会显示:
如果测试不通过就会显示:
正常项目的测试不会这么简单!
如何测试文件
单元测试一般是白盒测试,白盒的意思是『“我知道代码是怎么写的,测试的时候就按照写的来测”』
单元测试
单元测试node-todo-reagen
小试牛刀
我们要测试db.js,可以正常点的读文件,也可以正常的写文件
在根目录下新建__tests__
目录,这是Jest
的约定
在__tests__
目录中就可以写测试文件,新建db.sepc.js
,一般不叫db.test.js
因为太宽泛了,我们写的是单元测试,因此一般叫db.sepc.js 或 db.unit.js
备注: 单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。
【示例】
const db = require('../db.js')
describe('db',() => {
it('can read', ()=> {
expect(db.read instanceof Function).toBe(true)
});
it('can write', ()=> {
expect(db.write instanceof Function).toBe(true)
});
})
备注:
- 一般不用
test
而是用describe
描述要测试的对象 - 字面化的意思就是:『描述db,它能读,它能写』
思路
如何测试db可以读文件呢?
我们写一个文件,如果db能把这个文件读出来,那么就说明它可以读。即把文件写到硬盘上,让db去读。但是这样存在一个问题,如果硬盘上这个文件已经存在了,就会导致测试错误,这时候我们会以为是自己的代码写错了
也就是说如果我们在依赖外界环境做的测试,错的有可能是外界环境,而不是我们的代码写错了,因此单元测试不应该与外界打交道,也就是说我们在做单元测试的时候,不能操作网络,不能操作硬盘...,只能操作自己的文件
那么如果我们不写一个文件,又怎么读文件呢?
Jest帮我们做了一个假的fs
看下Mocking Node modules,因为fs是Node的模块
步骤:
- 在
__tests__
的同级目录,新建一个__mocks__
目录。在__mocks__
中新建一个fs.js
,mock的假的fs - 接下来要在
__tests__
文件夹中的db.spec.js
中使用上面mock的fs - 首先在用这个假的
fs
的时候,最好要把fs先声明一下const fs = jest.genMockFromModule('fs');
否则后面对fs的操作jest是不认的 - 接着要写
jest.mock('fs');
- 那么之后再
require('fs')
的时候这里的fs
就不是真的fs
而是假的fs
(上面mock的fs)
测试读文件代码:
// __mocks__ / fs.js
const fs = jest.genMockFromModule('fs');
fs.x = () => {
console.log('hello jest')
return 'xxx'
}
module.exports = fs
// __tests__ / db.spec.js
const db = require('../db.js')
const fs = require('fs')
jest.mock('fs');
describe('db',() => {
it('can read', function () {
expect(fs.x()).toBe('xxx')
});
})
运行结果
备注:
- 当写了
jest.mock('fs');
时那么就意味着Jest已经接管了fs
,所有对fs的操作已都会被阻止 const fs = jest.genMockFromModule('fs');
这句话的意思就是『“使用jest做的假的模块,是专门做测试用的”』- 注意:
const fs = require('fs') // 真的fs
jest.mock('fs'); // 周后用的fs都是假的fs
改造一下
我们要测db.read()
代码:
const db = require('../db.js')
const fs = require('fs')
jest.mock('fs');
describe('db',() => {
it('can read', function () {
db.read('/xxx')
});
})
db.read()
实际上调用的还是fs.readFile()
,我们是不是可以尝试在fs.readFile()
上做点手脚?
我们的fs能不能提供一个设置某个路径对应的error和data的函数呢?
任何人在read路径上的文件的时候,结果得到的error一定是空,结果得到的data一定是空字符串
'[]'
const db = require('../db.js')
const fs = require('fs')
jest.mock('fs');
describe('db',() => {
it('can read', function () {
fs.setMock('xxx',null,'[]')
db.read('/xxx')
});
})
那就开始干吧! 反正在__mocks__ / fs.js
中我们可以对这个假的fs做任何操作,我们可以造任何自己想要的模块
const fs = jest.genMockFromModule('fs');
const mocks = {}
fs.setMock = (path,error,data) => {
mocks[path] = [error,data]
}
module.exports = fs
意思是只要只要读path
就返回回调的参数
代码
// __mocks__ / fs文件
// 假的fs
const fs = jest.genMockFromModule('fs');
// 真正的fs
const _fs = jest.requireActual('fs')
Object.assign(fs,_fs)
const readMocks = {}
fs.setMock = (path,error,data) => {
readMocks[path] = [error,data]
}
// readFile的写法参考真的readFile的文档
fs.readFile = (path,options,callback) => {
// options参数,用户有可能不传
if(callback === undefined){callback = options}
if(path in readMocks){
callback(readMocks[path][0],readMocks[path][1])
}else {
_fs.readFile(path,options,callback)
}
}
module.exports = fs
// __tests__ / de.spec.js 文件
const db = require('../db.js')
const fs = require('fs')
jest.mock('fs');
describe('db',() => {
const data = [{title:'hi',done: false}]
it('can read', async ()=>{
fs.setMock('/xxx',null,JSON.stringify(data))
const list = await db.read('/xxx')
expect(list).toStrictEqual(data)
});
})
运行yarn test
或者点击运行按钮
测试结果:
这样我们就测试完了db.read()
功能,接下来要测试一下db.write()
功能!
备注
- 那我们怎么知道
fs.readFile
是怎么实现的呢? 如果把Node.js的fs.readFile
copy过来,我们也不知道源代码,该怎么办? Jest已经帮我们想好了!!
我们可以通过const _fs = jest.requireActual('fs')
获取真正的fs(真的fs是_fa
,假的fs是fs
)
-
接着使用
Object.assign(fs,_fs)
(将对象_fs
的所有key复制到左边的对象fs
中),将真的fs复制到假的mock的fs中。 因此假的fs(fs)和真的fs(已经是一模一样了),但是我们需要将fs中的readFile功能重新实现一下 -
如果你读的path正好在
mocks
中那么我们就不是直接使用Node中的fs.readFile而是应该在前面『拦截处理一下』,即如果我发现你的读的路径是被我mock过的,那么就不走真正的readFile。如果发现你读的路径不是被我mock过的,就走真正的readFile -
如果用户不传option是,那么callback就是undefined,如何解决这个问题呢?那么options就是作为callback
-
空数组是不等于空数组的(两个对象),不能使用
expect(list).toEqual([])
而是应该使用expect(list).toStrictEqual([])
(用来对比两个对象是否相等)
如何测试写文件
思路
如何知道已经测试了写文件?我们又不能真正写文件,因为单元测试不能和外界打交道
可不可以这样?写文件的时候不把内容写到文件里,而是把它写到一个变量里
改造一下writeFile
接下来要改造一下writeFile
【测试写文件代码:】
// mock逻辑
let writeMocks = {}
fs.setWriteFileMock = (path,fn) => {
writeMocks[path] = fn
}
fs.writeFile = (path,data,options,callback) => {
if(path in writeMocks){
writeMocks[path](path,data,options,callback)
}else{
_fs.writeFile(path,data,options,callback)
}
}
// 测试逻辑
it('can write',async () => {
let fakeFile
// data是写的内容
fs.setWriteFileMock('/yyy',(path,data,callback)=>{
// 写文件到变量fakeFile
fakeFile = data
// null表示没有错误
callback(null)
})
const list = [{title: '打乒乓球',done: true},{title: '打扫卫生',done: false}]
await db.write(list,'/yyy')
expect(fakeFile).toBe(JSON.stringify(list))
})
运行结果:
备注
-
fs.setWriteFileMock = (path,fn) => { ... }
,由于不能写内容到文件中,那应该做什么呢?函数fn就是如果不写文件,应该做的事情 -
下面的意思是:如果路径path在
writeMocks
那么就是希望使用mock,否则说明不希望使用mock,那么就调用真正的writeFile
if(path in writeMocks){
writeMocks[path](path,data,options,callback)
}else{
_fs.writeFile(path,data,options,callback)
}
- 如果访问的路径是mock的路径,那么就不是真正的写文件,而是写内容到变量
fakeFile
中 - 由于我们对
/yyy
路径进行了mock,因此并不会真正的写内容到/yyy
文件中,而是直接写到变量fakeFile
中 - 看下下面的代码由于wrie的结果必须等
fs.writeFile
中的回调被执行,才能拿到结果。因此,setWriteFileMock参数中必须要传参数callback,同时必须调用这个回调callback(null)
,参数为null表示没有错误,这样writeFile
才能完成
await db.write(list,'/yyy')
write(list,path = dbPath) {
return new Promise((resolve, reject)=>{
const string = JSON.stringify(list)
fs.writeFile(dbPath,string,(error) => {
if(error){
reject(error)
}else{
resolve()
}
})
})
}
fs.setWriteFileMock('/yyy',(path,data,callback)=>{
fakeFile = data
callback(null)
})
捋一下思路
- 第一步:db的读写功能是基于
fs.readFile()
和fs.writeFile()
因此要对这两个方法进行拦截,暂且拿读的功能来说明思路 - 第二步:重写writeFile,如果路径path在
writeMocks
说明当前执行了mock,那么就调用函fn(path,data,options,callback)
__mocks__ / fs.js
let writeMocks = {}
fs.setWriteFileMock = (path,fn) => {
writeMocks[path] = fn
}
fs.writeFile = (path,data,options,callback) => {
// options 用户有可能不传
if(callback === undefined){
callback = options
}
if(path in writeMocks){
writeMocks[path](path,data,options,callback)
}else{
_fs.writeFile(path,data,options,callback)
}
}
module.exports = fs
- 第三步:
// __tests__ / db.spec.js
it('can write',async () => {
let fakeFile
// data是写的内容
fs.setWriteFileMock('/yyy',(path,data,callback)=>{
// 写文件到变量fakeFile
fakeFile = data
// null表示没有错误
callback(null)
})
const list = [{title: '打乒乓球',done: true},{title: '打扫卫生',done: false}]
await db.write(list,'/yyy')
expect(fakeFile).toBe(JSON.stringify(list))
})
调用fs.setWriteFileMock(...)
后结果:
writeMocks = {'/yyy':(path,data,callback)=>{
fakeFile = data
callback(null)
}
}
执行db.write(list,'/yyy')
时由于'/yyy'
路径在writeMocks
中,结果就是执行函数(path,data,callback)=>{ fakeFile = data callback(null) }
得到的结果:
fakeFile = list
可是为什么这样能证明db.read()
是没问题的呢?全程都没用到fs.writeFile
一个测试习惯 ——— 清除mock
一般在mock完一个数据之后,就要清除掉mock,否则可能会影响到下一个测试,如:fs.setReadFileMock('/xxx',null,JSON.stringify(data))
在读的时候mock了/xxx
,如果下一个测试用例也用到了/xxx
,那么两个测试用例就相互影响了
因此我们要做一个清除mock
定义
fs.clearMocks = () => {
readMocks = {}
writeMocks = {}
}
使用
describe('db', () => {
afterEach(()=>{
fs.clearMocks()
})
...
})
afterEach(fn, timeout)
文件内每个测试完成后执行的钩子函数
高效阅读文档的能力
- 好的英文阅读能力
- CRM学习法,快速copy代码、修改代码、运行代码的能力
- 快速定位问题和快速找到问题对应在文档中解决的能力
转载自:https://juejin.cn/post/7247901054237458469