2. Mock事件发射器
模拟node的child_process.spawn方法略微有点复杂,因为它返回ChildProcess事件发射器。宝宝不用怕,我已经写了一个实用程序来stubspawn或者exec这些系统方法了。另外,我还打造了一件让测试更加友好的神器stub-spawn-once——每用一次,它都会自动清除掉stub。
我给你们打个样,看好喽~
./blame.js用以下命令调用./exec.js(假设文件名是foo.txt,文件内容为10行)
git blame --porcelain -L 10,10 foo.txt |
上述命令就会输出以下结果
6f272d6aef8a1d19e559792d9493d07d4218ea09 10 10 1 author Gleb Bahmutov author-mail <gleb.bahmutov@gmail.com> author-time 1499625750 author-tz -0400 committer Gleb Bahmutov committer-mail <gleb.bahmutov@gmail.com> committer-time 1499625750 committer-tz -0400 summary this is initial commit boundary filename foo.txt |
接下来, 我们在测试时,就可以mockfs.existsSync和child_process.exec这些Node内置接口方法,如下所示
const sinon = require('sinon') // we need access to "fs" object const fs = require('fs') const {stubExecOnce} = require('stub-spawn-once') // common-tags is a great library for dealing with whitespace // in ES6 template literals const {stripIndent} = require('common-tags') describe('blame', () => { const filename = 'foo.txt' const line = 10 beforeEach(() => { sinon.stub(fs, 'existsSync').withArgs(filename).returns(true) const cmd = 'git blame --porcelain -L 10,10 foo.txt' const blameOutput = stripIndent` // the mock output shown above ` stubExecOnce(cmd, blameOutput) }) afterEach(() => { // remove sinon's stubs fs.existsSync.restore() // stubExecOnce removes itself after single use }) it('returns blame information for given line', () => { return blame(filename, line) .then(info => { // check info object }) }) }) |
注意,现在我们已经不再关心或者依赖我们的内部模块了。取而代之的是,我们只关心和操作系统的交互。~duang~这样就比较容易把控啦。试想一下,一个开发者更新了./blame.js的测试,然后他就可以看到期望的git命令和mock输出结果,甚至还可以在终端执行命令。(貌似很酸爽哦~)
而且如果我们改变内部./exec.js 的代码,或者用其他库来替换它去执行shell脚本,我们测试依然执行同样的操作系统方法调用,应该也可以解析出同样的命令输出。现在对./blame.js的测试只要测试一个源文件就好,不用测试好几个了。
示例之Mock系统接口
走过路过不要错过,各位客官,来看看这些规范文件,它们介绍了如何mock稳健的NODE接口调用方法,再也不用费尽心神地去研究那些内部模块喽。
ggit/blame-spec.js
snap-shot 和schema-shot测试(见下面的福利篇)
cypress-io/env-or-json-file
如下stub文件下载
sinon.stub(fs, 'existsSync').withArgs(fullPath).returns(true) sinon .stub(fs, 'readFileSync') .withArgs(fullPath, 'utf8') .returns(configString) |
看完用nock来stub HTTP(S)请求,只想说那句经典台词 so easy~~ haha~~
const nock = require('nock') const description = 'cool project, bro' beforeEach(() => { nock('https://api.github.com') .get('/repos/no-such-user/does-not-exist') .reply(200, { description }) }) it('can mock non-existent repo', () => { const url = 'git@github.com:no-such-user/does-not-exist.git' return repoDescription(url).then(text => { la(description === text, 'wrong description returned', text) }) }) |
每一个nock拦截都会在每次使用后自动删除,所以我们就不用费精力考虑清除喽~
更多案例,请戳这里node-mock-examples
总结思考
mock内部模块是很难滴:他们由Node模块系统缓存,不稳定,而且没有文档(两眼一抹黑啊)
mock操作系统很简单哦;接口都很稳健,而且都文档化了
有sinon和stub-spawn-once 这些库让stub接口方法 so easy,妈妈再也不用担心了~~
福利篇
我喜欢用snapshot testing快速断言所有接收到的结果。只需要包装从blame(filename, line)返回的promise就可以了。
const snapshot = require('snap-shot') describe('blame', () => { beforeEach(() => { // ... }) it('returns blame information for given line', () => snapshot(blame(filename, line)) }) }) |
同理,在这种情况下,我们可以通过schema snapshots跳过mock操作系统结果,相较于确切的数值,我们对结果的模式更感兴趣。
const schemaShot = require('schema-shot') describe('blame', () => { // no setup is necessary! it('returns blame information for this file', () => schemaShot(blame(__filename, 1)) }) }) /* saves schema: $ cat __snapshots__/blame-spec.js.schema-shot exports['gets blame for 1 line of this file 1'] = { "$schema": "http://json-schema.org/draft-04/schema#", "type": "object", "properties": { "commit": { "type": "string", "required": true }, "author": { "type": "string", "required": true }, "committer": { "type": "string", "required": true }, "summary": { "type": "string", "required": true }, "filename": { "type": "string", "required": true }, "line": { "type": "string", "required": true } }, "additionalProperties": false, "list": false, "example": { "commit": "adfb30d5888bb1eb9bad1f482248edec2947dab6", "author": "Gleb Bahmutov", "committer": "Gleb Bahmutov", "summary": "move blame test", "filename": "spec/blame-spec.js", "line": "\tconst la = require('lazy-ass');" } } */ |
Happy testing !