前端单元测试总结

362 阅读8分钟

前言

此次单元测试(下文简称单测)选型,首先第一要点就是要适配我们目前的技术栈(vue2.x/3.0/react),其次就是通过社区活跃度github star数google搜索量等外部条件进行一个筛选。

技术栈适配

目前我们的项目基本都是基于vue进行开发,vue本身推荐了三种Unit testing方案供我们选择:mochaJESTkarma

搜索量

07e288e1639d100a36f84693a91f6c6.png

245b3b4ea1a0ae26dbeefa117da0b5d.png

2f0d8b8ce30f947a13ed2748258e25a.png

三者比较

测试框架名github star优点缺点
JEST34.6k有断言库、快照以及API丰富、社区活跃兼容不好
MOCHA20.4k历史悠久、灵活可与多个框架配合、文献多、测试报告丰富较新的领域,部分领域缺少支持
KARMA11.5k灵活需要与别的框架配合

结论

通过上述分析,最后我们选择了JEST作为我们的预备选型方案。

单测是什么?

单元测试是一种软件测试,其测试软件的各个单元或组件。目的是验证软件代码每个单元是否按照预期执行。单元测试由开发人员在应用程序的开发(编码阶段)中完成。单元测试隔离一段代码并验证其正确性。一个单元可能是单个功能,方法,过程,模块或对象。

为什么要使用单元测试?

在回答这个问题之前我们先想一下我们的什么样的一个开发流程?

image.png

这个流程似乎是没有什么问题,但是这时候新增了一个需求或需要对这个代码进行一个重构。而接手人并不是之前的开发者,无论是新需求与之前代码有耦合还是对代码进行重构,都会比较困难。因为新接手的人并不熟悉旧代码的逻辑以及业务,他得花大量的时间去了解旧代码、旧业务。 针对这个问题,就需要使用到我们的单元测试。因为单元测试的特性(与代码时刻保持一致),我们只需要在迭代项目时,跑测试用例,保证测试用例通过那我们的代码就不会有问题。

单测的优缺点是什么?

优点

  • 测试耦合性低,由于单元测试的模块化性质,我们可以测试项目的各个部分,而无需等待其他部分完成。
  • 自动化的单元测试避免了代码出现回归,编写完成之后,可以随时随地的快速运行测试。
  • 单元测试是一种无价的文档,它是展示函数或类如何使用的最佳文档。这份文档是可编译、可运行的,并且它保持最新,永远与代码同步。

缺点

  • 编写麻烦,需要花费更多的时间进行开发。
  • 主要针对某单元进行测试,对更广泛的一些错误可能无法捕获。

单测的一些名词说明

TDD 测试驱动开发

在编写真正实现功能的代码之前先编写测试,每次测试之后,重构完成,然后再次执行相同或类似的测试。该过程根据需要重复多次,直到每个单元根据所需的规格运行。

BDD 行为驱动开发

BDD将TDD的一般技术和原理与领域驱动设计(DDD)的想法相结合。 BDD是一个设计活动,您可以根据预期行为逐步构建功能块。

BDD的重点是软件开发过程中使用的语言和交互。

行为驱动的开发人员使用他们的母语与领域驱动设计的语言相结合来描述他们的代码的目的和好处。

使用BDD的团队应该能够以用户故事的形式提供大量的“功能文档”,并增加可执行场景或示例。 BDD通常有助于领域专家理解实现而不是暴露代码级别测试

断言

断言就是专门用来验证输出和期望是否一致的一个工具。在内容的实现上,它是通过比较一个实际值actual和一个期望值expected来实现的。

JEST 学习

JEST基础

断言库

jest是自带一个断言库的,具体可以看下JEST官方文档。这里,我就简单的说一下断言库的一个原理,我们先看一个栗子:

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

test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
});

上面这个栗子里expect(sum(1, 2)).toBe(3);这个就是一个断言,其实说白了就是jest提供了一个expect方法,我们现在自己模拟一个试一试:

function expect(fn) {
  return {
    tobe(val) {
      if (fn === val) {
        return true;
      } else {
        throw `toBe` + fn;
      }
    }
  };
}

看了上面这个例子,相信现在我们已经可以轻松的理解了断言库是什么,剩下的只需要去熟悉一下文档就可以了。

勾子函数

这个勾子函数的作用是什么呢,我们看个例子:

/*Db类*/
class Db{
  constructor(count){
    this.count = count;
  }

  add(){
    return ++this.count
  }

  minus(){
    return --this.count
  }
}

describe('测试没有勾子函数',() =>{
  let db = new Db(1)
  it('当没有勾子函数时,使用add()方法',()=>{
      expect(db.add()).toBe(2) // 第一个断言
  })
  
  it('当没有勾子函数时,使用add()方法',()=>{
      expect(db.minus()).toBe(1) // 第一个断言
  })
})

通过该例子,当我们新建db实例做测试时,我们的第二个断言第一个断言影响了。这样肯定是不对的,因为单元测试就是为了保证代码的独立性,这时候我们就可以用到我们的钩子函数beforeEach

describe('测试有勾子函数',() =>{
  let db
  beforeEach(()=>{
     db = new Db(1)
  })
  it('当没有勾子函数时,使用单例会如何',()=>{
      expect(db.add()).toBe(2)
  })

  it('当没有勾子函数时,使用单例会如何',()=>{
    expect(db.minus()).toBe(0)
  })
})

JEST为我们提供了4个勾子函数,分别是

  • afterAll(fn, timeout) 在测试用例执行结束之后运行
  • afterEach(fn, timeout) 在每一个测试用例执行之后运行
  • beforeAll(fn, timeout) 在测试用例执行之前运行
  • beforeEach(fn, timeout) 在每一个测试用例执行之前运行

测试异步代码

在测试的时候,我们经常会遇到一些数据请求事件点击定时器任务回调函数等等等,假如我们现在来写一个异步函数:

// 回调函数
function delay(callback,times=0){
  setTimeout(()=>{
    return callback('leehom')
  },times)
}

describe('测试回调函数',()=>{
  it('callback',()=>{
    delay(callback=>{
      console.log(callback)
      expect(callback).toBe('leehom');
    })
  })
})

如果我们这样写,我们会发现测试用例是通过,但是console.log里的内容却没有打印出来。因为它是一个异步函数,所以他并没有走到callback的地方就直接结束掉了。test里对于异步函数提供了一个done参数,这个参数可以标注你想在什么时候结束掉这个用例:

describe('测试回调函数',()=>{
  it('callback',(done)=>{
    delay(callback=>{
      console.log(callback)
      expect(callback).toBe('leehom');
      done()
    })
  })
})

mock

在项目中,一个模块的方法内常常会去调用另外一个模块的方法。在单元测试中,我们可能并不需要关心内部调用的方法的执行过程和结果,只想知道它是否被正确调用即可,甚至会指定该函数的返回值。此时,使用Mock函数是十分有必要。下面我们举个例子:

// 待测试函数
function testNoMock(params, fn) {
  fn(params);
}

describe("测试Mock",()=>{
  it('测试不用test',()=>{
    const demo = function(item){
      return item
    }
    expect(testNoMock([1,2],demo)).toEqual([1,2])//false
    console.log(testNoMock([1,2],noMockFun)) //值为undefined的原因是`testNoMock`并没有返回值
  })
})

我们只关注testNoMock()可以正常的调用callBack而不是关心noMockFun的执行过程,并且我们这个单测也是不通过的,因为test([1,2],noMocakFun)的值为undefined,其原因是testNoMock()并没有返回值。所以这个时候可以用到Mock方法:

describe("测试Mock",()=>{
  it('测试不用test',()=>{
    const mockFn = jest.fn()
    testNoMock([0,1],mockFn);
    expect(mockFn).toBeCalled() //通过
  })
})

快照测试 没时间晚点写

JEST 配置说明

module.exports = {
  // 识别这些后缀
  moduleFileExtensions: [
    'js',
    'jsx',
    'json',
    // tell Jest to handle *.vue files
    'vue'
  ],
  transform: {
    // process *.vue files with vue-jest
    '^.+\\.vue$': require.resolve('vue-jest'),
    '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$':
    require.resolve('jest-transform-stub'),
    '^.+\\.jsx?$': require.resolve('babel-jest')
  },
  // 不转化的代码
  transformIgnorePatterns: ['/node_modules/'],
  // support the same @ -> src alias mapping in source code
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1'
  },
  testEnvironment: 'jest-environment-jsdom-fifteen',
  // serializer for snapshots 快照
  snapshotSerializers: [
    'jest-serializer-vue'
  ],
  // 匹配某个目录下 *.space格式的文件
  testMatch: [
    '**/tests/unit/**/*.spec.[jt]s?(x)',
    '**/__tests__/*.[jt]s?(x)'
  ],
  // https://github.com/facebook/jest/issues/6766
  // 模拟浏览器地址是
  testURL: 'http://localhost/',
  watchPlugins: [
    require.resolve('jest-watch-typeahead/filename'),
    require.resolve('jest-watch-typeahead/testname')
  ]
}

Vue Test Utils

前端做单元测试,一般都需要配合一些utils库去对dom/vm实例等进行操作,所以这里我们使用vue官方提供的utils库.它为我们提供了mount()shallowMountrender()等非常好用的方法,我们可以在Vue Test Utils 2.X官方文档上去学习如何使用并且里面有相关的一些Demo给我们参考。

自定义 Test-Utils

可以参考

Element Utils库

vue-test-utils源码

运用环节

这一部分分为上文提到过 TDD以及BDD来说明,开发一个简单的组件,分别用这两种方式进行自动化测试的编写。

功能需求 完成一个搜索框,有以下功能:

  • 1.有字数限制并默认值为空
  • 2.如果搜索框为空则不操作
  • 3.输出回车后,清空input
  • 4.缓存历史记录,获焦时展示历史搜索

本文代码仓库

暂时没有整理好.