单元测试之文件模块

如何测试

步骤

  • 选择测试工具: 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

测试node-todo-reagen项目的db.js文件

小试牛刀

我们要测试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.readFilecopy过来,我们也不知道源代码,该怎么办?
    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)  
})

捋一下思路

  1. 第一步:db的读写功能是基于fs.readFile()fs.writeFile() 因此要对这两个方法进行拦截,暂且拿读的功能来说明思路
  2. 第二步:重写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
  1. 第三步:
// __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 作者:一直酷下去

(0)
上一篇 2023年6月23日 上午11:15
下一篇 2023年6月24日 上午10:05

相关推荐

发表回复

登录后才能评论