soda-x / blog

Here is my blog
753 stars 37 forks source link

在 es6 中 sinon 的正确打开方式 #8

Open soda-x opened 6 years ago

soda-x commented 6 years ago

最近一周在补前人留下的测试用例,然后碰到了一个 sinon 的使用问题困扰了一整天,特做此记录。

文章的前提是:你对 sinon 已经有了初步的使用经验。

// src.js
function add(a, b) {
  return a + b;
}

function multiplication(a, b) {
  return a * b;
}

export function complex(a, b) {
  console.error('此处为 error 发生出');
  return add(a, b) + multiplication(a, b);
}

export default function division(a, b) {
  return add(a, b)/multiplication(a, b);
}
//test.js
import test from 'ava'; // eslint-disable-line
import sinon from 'sinon'; // eslint-disable-line

import { complex }, division from '../src';

let sandbox;

test.before(() => {
  sandbox = sinon.sandbox.create();
});

test.after(() => {
  sandbox.restore();
});

test('complex', (t) => {
  sandbox.stub(console, 'error');
  // how to stub add or multiplication ?
});

碰到第一个问题是:如何 stub 非 export 的方法呢 ?另外 stub 的 api 要求,method 需要挂载在相应的 object 下

无奈之下我选择了最偷懒的方式把 addmultiplication 都修改为 export 的方法,使用了 import * 的方式

即修改为了

// src.js
export function add(a, b) {
  return a + b;
}

export function multiplication(a, b) {
  return a * b;
}

export function complex(a, b) {
  console.error('此处为 error 发生出');
  return add(a, b) + multiplication(a, b);
}

export default function division(a, b) {
  return add(a, b)/multiplication(a, b);
}
//test.js
import test from 'ava'; // eslint-disable-line
import sinon from 'sinon'; // eslint-disable-line

import * as math  from '../src';

let sandbox;

test.before(() => {
  sandbox = sinon.sandbox.create();
});

test.after(() => {
  sandbox.restore();
});

test('complex', (t) => {
  sandbox.stub(console, 'error');
  sandbox.stub(math, 'add').returns(2);
  sandbox.stub(math, 'multiplication').returns(3);
  t.true(console.error.called);
  t.is(math.complex(2, 3), 5);
  // 用例挂了 t.is(11, 5);
});

偷懒方式破灭,发现 math.complex(2, 3) 返回的是 11,也就是 stub math add 和 multiplication 并没有生效!但是关键是 console.error 被正常改写。到底发生了什么!所以我猜想 stub 的 add 和 multiplication 方法并不是 complex 方式调用的 add 和 multiplication

由于并不清楚发生了什么,所以猜测是不是因为引用关系的问题

所以我开始尝试,改写 src.js 尽量保证引用关系

let math;

function add(a, b) {
  return a + b;
}

function multiplication(a, b) {
  return a * b;
}

function complex(a, b) {
  console.error('此处为 error 发生出');
  process.exit(1);
  return math.add(a, b) + math.multiplication(a, b);
}

math = {
  add,
  multiplication,
  complex,
};

export { math };

然而答案居然是成功了!!!!但回过头必须要思考的事情是:1.这种处理方案并不完美,原因在于为了写测试需要变更源码本身相对优雅的写法,同时会暴露无关的内部函数 2. 为什么 直接 export 到具体的 function 不行,但是 export 到 对象就行了,这或许并不是引用关系的问题。

带着这个思考,我往这个方向 google 了下,非常有意思我发现了在 stackoverflow 上的提问 https://stackoverflow.com/questions/35240469/how-to-mock-the-imports-of-an-es6-module

其中在非常不显眼的地方我居然看到了问题最最最关键的内容,

@carpeliam This wont work with the ES6 module spec where the imports are readonly.

import 的内容是 readonly 的!!!!!!!但是其内部 child 不是 readonly 的!!!!!!!至此豁然开朗!!!!


接下来我就开始想,如果说这是因为 spec 的原因导致,那么万能的 babel 解决这个问题肯定易如反掌,所以我开始尝试搜索这方面的 babel-plugin 。结果当然是 wala babel-plugin-rewire

因为找对了方向,所以问题的解决方式也越合规。其中认为最合适的是how to stub ES6 module dependencies

最佳实践

// src.js 不需要对 源码 文件作出任何的调整
function add(a, b) {
  return a + b;
}

function multiplication(a, b) {
  return a * b;
}

export function complex(a, b) {
  console.error('此处为 error 发生出');
  return add(a, b) + multiplication(a, b);
}

export default function division(a, b) {
  return add(a, b)/multiplication(a, b);
}
import test from 'ava'; // eslint-disable-line
import sinon from 'sinon'; // eslint-disable-line

import * as math from '../src';

let sandbox;

const rewire = (module, methodName, method) => {
  module.__Rewire__(methodName, method);

  return method;
};

test.before(() => {
  sandbox = sinon.sandbox.create();
});

test.after(() => {
  sandbox.restore();
});

test('complex', (t) => {
  sandbox.stub(console, 'error');
  rewire(math, 'add', sandbox.stub())
    .returns(2);
  rewire(math, 'multiplication', sandbox.stub())
    .returns(3);

  t.true(console.error.called);
  t.is(math.complex(2, 3), 5);
});

补充课外题

当如果 src.js 中我们 import 了 一个 util 的方法

// src.js

import { chalk } from 'util';

export default function log() {
  console.log(chalk.yellow('yellow log'));
}

请问如何测试 chalk.yellow 被正确调用了?

ziluo commented 6 years ago

最近挺高产的,小JJ

soda-x commented 6 years ago

@ziluo 课后习题做了么 ☺

ziluo commented 6 years ago

示例代码里面build没定义啊 👻

soda-x commented 6 years ago

@ziluo fix 了 XD

cjzcpsyx commented 6 years ago

目前见到写的最明白的es6 dependency stub竟然是一篇中文post,厉害了 是某蚂蚁金服的技术大佬么 😃

soda-x commented 6 years ago

@cjzcpsyx 是否有意来我厂 😁