手摸手带你走入前端单元测试

avatar
@https://www.tuya.com/

本文由团队成员 佐菲 撰写,已授权涂鸦大前端独家使用,包括但不限于编辑、标注原创等权益。

背景

测试是为了保证研发产品交付物质量的重要手段,也是软件生命周期的一部分 研发同学在此周期也是重要的参与者,毕竟 bug 都是同学们写的🐒 那么研发同学该如何理解测试呢?

测试简介

我们大致可以从两方面对测试分类,测试种类和测试阶段

测试种类

  1. 功能测试:主要验证功能是否符合需求,是否遗漏
  2. 性能测试:主要关注系统提供能力,比如后端服务关注的 QPS, TPS,前端关注的 FCP,FMP 等指标
  3. 健壮性测试:主要关注系统的边界数据情况,非法数据会不会引起系统崩溃等
  4. 安全测试:主要是验证产品是否符合安全需求定义和产品质量标准的测试

测试阶段

  1. 单元测试:本阶段主要是研发人员对当前最小可用功能的检查与验证
  2. 集成测试:单元测试通过后,对由几个子功能组成的子系统的测试
  3. 回归测试:检查已有功能的正确性检查
  4. 系统测试:在产品发布前做的最后一轮测试,对整个系统进行测试

测试种类与测试阶段的关系

测试阶段测试类型参与人员
单元测试功能测试、健壮性测试研发同学
集成测试功能测试、健壮性测试研发同学
回归测试功能测试、健壮性测试、安全测试研发&测试同学
系统测试功能测试、性能测试、健壮性测试、安全测试测试同学

举个栗子

假设我们现在有一部电梯

功能测试:开门、关门、报警电话、轿厢上升、轿厢下降、不同楼层停留、不同楼层呼叫 健壮性测试:同时按上下呼叫按钮、不选楼层,选择全部楼层、载重量测试、停电测试 安全测试:超载报警测试,轿厢门阻塞报警测试 性能测试:1层到最高层空载耗时,1层到最高层满载耗时

市场情况

回归正题,测试在前端领域该怎么做,有哪些工具来帮助我们完成测试 对前端同学来说,前端测试分为两种,UT 和 E2E UT 测试就是我们常说的单元测试,我们围绕着功能 module 以及 function 来编写 test case,组合这些 test case 形成 test suits,来完成我们的单元测试 目前主流测试框架:jest, mocha, jasmine

E2E 测试,是 End-to-End 测试,在此阶段,我们更关注整个操作链路的正确性 目前主流测试框架:playwright, pupeteer, nightwatch, phantomjs, selenium

选型

简便、通用、易学、高可扩展是框架选择的原则 结合市场情况,选择目前市场认可度最高的jest框架 jest 是由 Facebook 出品,集成了 expect, chalk, jsdom, jasmine, sinon 等测试库,同时通过一些插件,可以打通 E2E 测试,实现 UT 与 E2E 的全覆盖测试

jest 简介

jest 是一个驱动框架,负责运行测试用例,展示测试结果。我们是以 module 为单位书写具体的测试用例,每个 module 为一个独立的测试环境,jest 测试套件生命周期有 beforeAll, beforeEach, afterAll, afterEach 基本 api 有 describe, test, expect

详细api地址:jestjs.io/zh-Hans/doc…

来个栗子🌰:

由于 jest.useFakeTimers() 在 CodeSandBox 运行异常,例子 2 的 test suits 跑不通,请 git clone 到本地运行

import {initConfig, login, resetPassword, getChildrenAssetsByAssetId, addAsset} from '../src/utils';

// 此module所有测试用例运行前执行
beforeAll(() => {
  initConfig();
})

describe('account test', function () {
  describe('#login()', function() {
    test('should return true', function () {
      return login('test', 'test').then((res) => {
        expect(res).toBeTruthy();
      });
    })
  })

  // describe可以嵌套
  describe('#resetPassword()', function () {
    test('should return true', function() {
      return resetPassword('test', 'test', 'test1').then((res) => {
        expect(res).toBeTruthy();
      })
    });

    // describe中可以包含多个test case
    test('should return assets array', function () {
      return getChildrenAssetsByAssetId('1').then((res) => {
        return expect(res).toEqual([{
          assetId: '11',
          assetName: '11',
        }]);
      })
    })
  })
})

// test case可以脱离describe运行
test('should return true', function () {
  return login('test', 'test').then((res) => {
    expect(res).toBeTruthy();
  });
})

test('should return full resp', function() {
  return addAsset('1', '1', {
    responseRaw: true,
  }).then((res) => {
    return expect(res).toMatchObject({
      data: {
        success: true,
        code: 200,
        msg: '',
        result: true,
      },
      status: 200,
      statusText: 'OK',
    })
  })
})
  • 通过 expect 断言检测结果,参考上例
  • 支持异步处理,参考上例
  • 通过 Mock 方法,让测试更集中在待测试 function 本身, 参考例子2
  • 使用 snapshot,测试 ui 渲染的一致性,参考例子2
  • 测试覆盖率报告,jest --coverage

功能代码:

// example 2
// mock function + snapshot
// src/Clock.jsx
import React, {useEffect, useState} from 'react';

const Clock = () => {
  const [now, setNow] = useState(Date.now());

  useEffect(() => {
    const timer = setTimeout(() => {
      setNow(Date.now());
    }, 1000);

    return () => {
      clearTimeout(timer);
    }
  }, [])

  return (
    <div>
      timestamp is {now}
    </div>
  );
}

export default Clock;

测试代码:

// test/Clock.test.js
import React from 'react';
import render from 'react-test-renderer';
import Clock from '../src/Clock';

jest.useFakeTimers();
Date.now = jest.fn(() => 1615972229101);

beforeEach(() => {
  jest.clearAllTimers();
})

test('clock snapshot', () => {
  const tree = render.create(<Clock />).toJSON();

  expect(tree).toMatchSnapshot();
})

test('setTimeout tests', () => {
  const tree = render.create(<Clock />).toJSON();
  expect(setTimeout).toHaveBeenCalledTimes(2);
  expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1800);
})

测试结果:

接下来,我们更新 Clock.jsx,line 18 文本有更新。这时 snapshot 结果发生变化,我们需要手动更新一下 snapshot 结果,否则测试用例还是会使用老版本的 snapshot 作为参照,导致测试结果不符合预期:

import React, {useEffect, useState} from 'react';

const Clock = () => {
  const [now, setNow] = useState(Date.now());

  useEffect(() => {
    const timer = setTimeout(() => {
      setNow(Date.now());
    }, 1800);

    return () => {
      clearTimeout(timer);
    }
  }, [])

  return (
    <div>
      the timestamp is {now}
    </div>
  );
}

export default Clock;

没有更新 snapshot 时:

更新 snapshot,并测试:

./node_modules/.bin/jest -u

test3.png

总结

测试需要投入大量资源开发维护,短期收益并不大,但从长远来看,可以帮忙开发和测试同学节省大量回归测试时间。同时,UT 测试还应该是研发同学自信的来源。UT 测试不一定适应每个项目和团队,短周期一次性项目和团队研发资源不足的场景就不太适合在 UT 测试投入资源。所以,还需结合项目实际情况和团队资源综合评估判断