【学习】前端测试 jest (二)

709 阅读7分钟

上一篇:【学习】前端测试 jest (一)

4. redux-saga的测试

我发现 redux-saga 官方的测试用例使用的是 tape ,所以我先学会使用这个,然后看怎么使用 jest 来完成。安装就不用说了,直接上代码:

export function* syncAdd() {
  yield put({type: ADD});
}

对于这个简单的怎么写测试呢,很简单,我们只要不要想这个是 saga ,而是 js 代码就可以了,像这些 js 框架或者库什么的都当成 js ,其实本来也是 js ,但有时候可能有更好的测试方式,如果没有就当 js 进行:

import test from 'tape'; // 引入这个库
test('测试普通 saga', (t) => {
  const generator = syncAdd();
  t.deepEqual(generator.next().value, put({type: ADD}), '这是什么玩意');
  t.ok(generator.next().done, '所有的执行完成');
  t.end();
});

这个测试好像没什么用,一般都是有传值的,于是有下面的:

export function* syncMul(count) {
  yield put({type: MUL, count});
}

测试:

test('测试带有参数的 saga', (t) => {
  const count = 10;
  const generator = syncMul(count);
  t.deepEqual(
    generator.next().value,
    put({type: MUL, count: 10}),
    '这是带有参数的值为10',
  );
  t.ok(generator.next().done, '所有的执行完成了--');
  t.end();
});

写了这么多,应该怎么测试这些呢?可以照葫芦画瓢,先装下面的库:

yarn add @babel/node @babel/core cross-env tap-spec --dev
或
npm install @babel/node @babel/core cross-env tap-spec -D

然后在 package.json 文件的 “ scripts ” 下写一个属性,假如叫: tape ,像下面:

{
	...
	"scripts": {
		...
		"tape": "cross-env NODE_ENV=test babel-node src/app/sagas/counts.tape.js | tap-spec"
	},
	...
}

然后执行:

yarn tape
或
npm run tape

这个时候就会看到通过的 除了上面的这种方法还可以直接执行下面的语句:

npx babel-node 你的测试文件

只不过得到的界面有点丑 接下来就是异步了,一般使用 redux-saga 是为了写异步代码,所以下面的可能更重要,只不过使用这个 tape 也很简单:

function delay(time, count) {
  return new Promise((resolve, reject) => {
    let timer = setTimeout(() => {
      time && clearTimeout(timer);
      resolve(count + 10);
    }, time);
  });
}

export async function request(count) {
  const tempCount = await delay(1000, count);
  return tempCount;
}

export function* asyncAdd() {
  const count = yield call(request, 10);
  yield put({type: ADD, count});
}

我们可以看到我写的异步函数是定时器模拟的,返回的就是传入的数据 + 10 。

test('测试异步的 saga', (t) => {
  const generator = asyncAdd();
  let count;
  t.deepEqual(
    generator.next(count).value,
    call(request, 10),
    '开始调用异步的请求',
  );
  t.deepEqual(
    generator.next().value,
    put({type: ADD, count}),
    '将获取得到的值传入 redux 中',
  );
  t.ok(generator.next().done, '所有的执行完成了--');
  t.end();
});

这里注意一下返回值,只需要将要返回的值放到 generator.next(count).value 中,这样就自然拿到值了,于是就可以开开心心的传入的 put({type: ADD, count}) 中了。

如果看到这里还是没明白的,我们来看看 deepEqual 这个函数:

// 先看 tape 层面的
function tapeDeepEqual(a, b, msg, extra) {
    this._assert(deepEqual(a, b, { strict: true }), {
        message: defined(msg, 'should be equivalent'),
        operator: 'deepEqual',
        actual: a,
        expected: b,
        extra: extra
    });
}
Test.prototype.deepEqual
= Test.prototype.deepEquals
= Test.prototype.isEquivalent
= Test.prototype.same
= tapeDeepEqual;
// 再看 deep-equal 的
function deepEqual(actual, expected, options) {
  var opts = options || {};

  // 7.1. All identical values are equivalent, as determined by ===.
  if (opts.strict ? is(actual, expected) : actual === expected) {
    return true;
  }

  // 7.3. Other pairs that do not both pass typeof value == 'object', equivalence is determined by ==.
  if (!actual || !expected || (typeof actual !== 'object' && typeof expected !== 'object')) {
    return opts.strict ? is(actual, expected) : actual == expected;
  }

  /*
   * 7.4. For all other Object pairs, including Array objects, equivalence is
   * determined by having the same number of owned properties (as verified
   * with Object.prototype.hasOwnProperty.call), the same set of keys
   * (although not necessarily the same order), equivalent values for every
   * corresponding key, and an identical 'prototype' property. Note: this
   * accounts for both named and indexed properties on Arrays.
   */
  // eslint-disable-next-line no-use-before-define
  return objEquiv(actual, expected, opts);
}

可以看到:

// tape 帮我们传入了 strict 并且值为: true
function tapeDeepEqual(a, b, msg, extra) {
    this._assert(deepEqual(a, b, { strict: true }), {
        message: defined(msg, 'should be equivalent'),
        operator: 'deepEqual',
        actual: a,
        expected: b,
        extra: extra
    });
}
// 和 deep-equal 库的关于这段的判断
// 7.1. All identical values are equivalent, as determined by ===.
if (opts.strict ? is(actual, expected) : actual === expected) {
  return true;
}

我们就不往下追究了,直接看看官方的定义: 也就是如果传入的是 true ,那么就使用的是递归比较,否值就是值比较。

看到这里应该看到测试的规则,也就是首先获得 generator ,然后就进行比较 generator.next(count).value 和 真正执行的,前一个是实际值,后一个是期望值,如果不相等说明就出现问题了。

下面我们看怎么使用 jest 来完成测试。 这个时候就把 redux-saga 看成是彻头彻尾的 js 函数,那么测试就很好进行了。首先尝试最简单的测试,下面我会把具体的函数实现和测试写到一块方便比较对比:

export function* syncAdd() {
  yield put({type: ADD});
}

// 测试
it('普通的 saga 的测试', () => {
  const generator = syncAdd();
  expect(generator.next().value).toEqual(put({type: ADD}));
});

发现好像更简单了,下面看看带有参数的:

export function* syncMul(count) {
  yield put({type: MUL, count});
}

// 测试
it('测试带有参数的 saga 的测试', () => {
  const count = 10;
  const generator = syncMul(count);
  expect(generator.next().value).toEqual(put({type: MUL, count: count}));
  expect(generator.next().done).toEqual(true);
});

这里的 toEqual 就是递归比较,所以效果跟上面的相同,具体的差别还需要看 toEqual 和 deepEqual 函数实现的差别。

其他的就先不写了,都差不多。以后实战会把遇到的坑和现在没注意到的地方都记录下来。

5. 组件的测试

对于前端来说,这个才是重头戏,毕竟逻辑什么的一般都是后台的,前端能处理的逻辑也不会太多,最主要的是如果把大量逻辑放到前端,即便逻辑处理和页面渲染分开也会导致页面等情况,只不过小逻辑还是自己处理比较好,跟后台反复通信的代价是昂贵的。由于我负责的是 react-native 的工作,所以我主要先弄这个,其他的后面慢慢补上,有可能没有,如果谁刚好负责那一块也写好了博客,可以告诉我,我把链接放到这里,方面不明白的人能够很容易的找到。

首先到 jest 官网的 react-native 那一部分看看: Testing React Native Apps 。根据上面的操作即可,只不过默认情况下是支持的,如果你的版本太低了就不要考虑了,官方说只支持 0.38 以上的,你可以检查一下你的 package.json 来看看是否支持,这里都有可以看一下。

如果你是混合开发,可能跟我们直接使用 react-native 的不一样,只不过没关系,应该都是大同小异。

先写一个简单的:

const TestFunction = () => {
  useEffect(() => {
    add(1, 2, 3, 4);
  }, []);
  const onClick = useCallback(() => {
    console.log('似乎就是这样,我修改一下函数的定义');
  }, []);
  return (
    <View style={styles.container}>
      <Text>这是学习 jest 测试。</Text>
      <Pressable onPress={onClick}>
        <Text>按钮</Text>
      </Pressable>
    </View>
  );
};

测试代码:

it('测试简单的组件 TestFunction', () => {
  const tree = renderer.create(<TestFunction />).toJSON();
  expect(tree).toMatchSnapshot();
});

这个代码很简单,如果你是第一次写,那么会在你的对应文件夹下生成 __snapshots__ 文件夹,下面会生成一个文件 index.test.js.snap 的类似文件,这个文件其实就是快照文件,如果下次修改了,那么就会出现测试失败,此时就需要你检查一下修改是不是无意改动还是有意改动,或者有没有改错什么的。

这里说的修改,指的是样式和组件的修改,不包含函数的修改,比如上面, useEffect 里面的修改并不会影响, 同时我修改了 add 函数也不会影响, 同时修改了 onClick 函数也不会影响。

6. 总结

第一次的学习就此结束,下面会有关于 jest 的更多用法,也就是相关钩子的使用。同时全部完成以后,我会出实践的,实践类的是和项目高度挂钩的,出现的任何问题都会记录,同时也会随着更新迭代,相关的记录也会保存。我之所以做这一整套学习记录,实践记录和迭代记录等,主要是我发现关于测试类的教程真的少,即便有也都是写介绍语法的,没有和项目挂钩,最先开始的时候就看得我云里雾里,真的还不如看官网。如果说的不明白的望看到的人提出来,我会尽可能的写详细,让大家都能看懂,而不是云里雾里。有时候网上的教程之所以出现很多云里雾里的教程,最大程度就是因为,他学会了再看的时候发现很多都觉得没必要讲或者不值得讲,这也是从学习的一刚开始就选择记录,不然我也会出现这样的情况。