71

Vue 应用单元测试的策略与实践 02 - 单元测试基础

 5 years ago
source link: http://blog.jimmylv.info/2018-10-29-vue-application-unit-test-strategy-and-practice-02-how-jest-work/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文的目标

  1. 在 TDD 做完 Tasking 列完实例化数据之后,完全没有UT基础不知道该怎么写单元测试?
// Given
一个完全没有UT基础的新人:walking:
// When
当他:walking:阅读和练习本文的Jest的部分
// Then
他能够把Given/When/Then的套路学会
他能够学会Jest的基本用法,包括测试suite和断言等语法
他能够学会Jest中测试异步的几种方式

单元测试基础

在上一篇文章当中我们介绍了单元测试的意义,以及为何选择 Facebook 的 Jest 作为我们的测试框架。现在就让我们一起来学习如何编写最基础的单元测试。

如果你已经有了使用 Jest 编写单元测试的经验,可以选择直接跳到第二段。

第一个 Jest 实例

首先创建 jest-demo 项目并安装 jest 作为项目 devDependencies 依赖:

mkdir jest-demo && cd $_
yarn init -y #--yes
yarn add jest -D #--dev

然后创建一个 math.js 文件,输入一个我们稍后测试的 sum 函数:

const sum = (a, b) => a + b

module.exports = { sum }

接下来,让我们写第一个测试。在同一个文件夹中创建一个 math.test.js 文件,在这里我们将使用 Jest 来测试 math.js 中定义的函数:

const { sum } = require('./math')

describe('Match module', () => {
  it("should return sum result when one number plus another number", () => {
    // Given
    const number = 1
    const anotherNumber = 2
    // When
    const result = sum(number, anotherNumber)
    // Then
    expect(result).toBe(2)
  })
})

然后运行 yarn test 你就可以看到相应的结果。

yYzYbuj.png!web

Given/When/Then 的套路

麻雀虽小五脏俱全,在上面的例子当中,我们可以看到很多的测试元素,下面将会一一介绍:

首先我们看到的是一个由 it 包裹的测试主体最小单元,采用了Given When Then的经典格式,我们常常称之为测试三部曲,也可以解释为 3A 即:

GWT 3A 说明 Given Arrange 准备测试测试数据,有时可以抽取到 beforeEach When Act 采取行动,一般来说就是调用相应的模块执行对应的函数或方法 Then Assert 断言,这时需要借助的就是Matchers的能力,Jest还可以扩展自己的Matcher

expect 后面的 toBe 称之为 Matcher,是断言时的判断语句以验证正确性 :white_check_mark:,在后面的文章中我们还会接触更多 Matchers,甚至可以扩展一些特别定制的 Matchers。

expect(1+1).toBe(2)
expect(1+1).not.toBe(3)

修改断言的结果,就可以看到成功后的结果了:

aamYvqu.png!web

模块间依赖 Fake/Stub/Mock/Spy

VfIjq23.png!web

如同人类世界中的羁绊,软件模块之间必然也免不了依赖。 Martin FowlerUnitTest 这篇文章当中将单元测试作了一个重要的区分,即你所测试的单位应该是社交型(Social Tests)还是独立型(Solitary Tests)? 想象一下你正在测试一个 Order Class 的 price() 方法,而 price() 方法需要在 ProductCustomer Class 中调用一些函数。如果你希望单元测试所测试的 Order 模块是独立的,那么你就不想直接使用真正的 ProductCustomer Class,因为 Customer Class 的错误会直接导致 Order Class 的单元测试失败。相反,你可能会使用一个替身作为依赖的对象,也就是我们接下来会提到的 Fake/Stub/Mock/Spy。

现实世界里,我们在写代码和单元测试时,常常遇到的一些需要替身的对象包括:

  • Database 数据库
  • Network requests 网络请求
  • access to Files 存取文件
  • any External system 任何外部系统

其实在 Jest 当中,Fake/Stub/Mock/Spy 这些概念或许会有所混淆,而这跟 JavaScript 语言本身的特点有一定关系,但是我觉得 Jest 通过统一的 fn() 方法把问题解决得还比较恰当,让我们来一块儿看看实例:chestnut::

Mock 用于替代整个模块

import SoundPlayer from './sound-player';

const mockPlaySoundFile = jest.fn();

jest.mock('./sound-player', () => {
  return jest.fn().mockImplementation(() => {
    return {playSoundFile: mockPlaySoundFile};
  });
});

比如说,我们可以看到 jest.mock() 方法中的第二个参数是一个函数,那么我们就可以完全接管整个 ./sound-player JavaScript 模块,比如说这里的 playSoundFile 本来应该是从 ./sound-player 这个文件当中 export 出来的,而被 Mock 之后我们的测试就可以使用 Mock 所返回的数据或方法,从而保证模块所返回的内容是我们所期望的。但这时需要注意的是,该模板的所有功能都已经被 Mock 掉,而不会再从原模块当中返回,所以我们就需要重新实现该模块中的所有功能。可别一不小心就成了张艺谋导演《影》片中的影子,被完全“取而代之”,连夫人也被 Mock 所吸引。

Stub 用于模拟特定行为

const mockFn = jest.fn();
mockFn();
expect(mockFn).toHaveBeenCalled();

// With a mock implementation:
const returnsTrue = jest.fn(() => true);
console.log(returnsTrue()); // true;

这里的特定行为也可以是没有行为, jest.fn() 代表着我就是一个 Stub(桩),“你来我就在这里,你走我也依然在这里,风雨无阻”。不需要什么输入输出,只要能在测试的时候验证到 Stub 被调用过就行,也就能够断言到某处代码被执行,从而确定代码被测试所覆盖。而另一种特定行为就是返回特定的数据,即 Stub 也可以根据输入模拟返回一种输出,作为某些模块的替身帮它演戏,比如“小鲜肉们”遇到要跳车啦、要

卿卿我我

(误)的时候就要找替身,“一二三四五六七八”连台词都不用背还需要配音。

Spy 用于监听模块行为

Spy packages without affecting the functions code
const video = require('./video');

it('plays video', () => {
  const spy = jest.spyOn(video, 'play');
  const isPlaying = video.play();

  expect(spy).toHaveBeenCalled();
  expect(isPlaying).toBe(true);
})

Spy 并不会影响到原有模块的功能代码,而只是充当一个监护人的作用,“你可以继续我型我秀上课讲小话,但是老师会偷偷告诉你妈妈,看你放学后老妈不打断你的腿”。比如说上文中的 video 模块中的 play() 方法已经被 spy 过,那么之后 play() 方法只要被调用过,我们就能判断其是否执行,甚至执行的次数。

如何 Mock 全局的方法

把全局的数据 Mock 掉很简单,只需要像 window.document.title = undefined 这样简单 Fake 赋值就很完美。而像 matchMedia 这样的方法在 jsdom 里面并没有被实现,这时候我们当然就需要去把它 Mock 掉,简单把要用到的一些对象属性赋值就好,总之不至于在运行时报错。

window.matchMedia = jest.fn().mockImplementation(query => {
  return {
    matches: false,
    media: query,
    onchange: null,
    addListener: jest.fn(),
    removeListener: jest.fn(),
  };
});

代码模块的易测性

从上文的一些例子当中,我们也可以看到,不管是 Fake/Stub/Mock/Spy 最最重要的一个原则就是「简单」,因为我们是在写测试代码,而所依赖的模块就应该以最简单的形态展现出来,绝不要给 jest.fn() 编写

过于 哪怕一点点复杂的逻辑。如果这个模块有多种表现形态,那就把它分种测试单元进行多次 Mock,每个 it()

单元测试一定是针对于单个功能点进行测试的。

保持单元测试独立性的同时,也是在促使你去思考什么样的模块才是符合「职责单一原则」的。单元测试站在使用者的角度来使用该模块,而代码的易测性也就代表着代码的可维护性。

如何测试异步代码?

异步是 JavaScript 中绕不开的永恒话题,多亏了 ES6 + 高级语法所提供的多种优雅的异步代码方式,让我们写测试代码的方式也多了好多种。(逃

让我们先来看一下什么是异步请求,这里有一个通过 Chrome API 获取当前位置的实例,可想而知 Chrome 要根据 GPS 信号才能算出当前的经纬度,相当于从卫星 来回走了一遭,怎么不会异步(代表有延时,延迟返回)呢?

navigator.geolocation.getCurrentPostion() # chrome API 异步获取当前位置

Callback 回调函数

it('the data is peanut butter', done => {
  function callback(data) {
    expect(data).toBe('peanut butter');
    done();
  }

  fetchData(callback);
});

这是最最普通的方式,也是各大框架都支持的一种写法, done() 作为异步代码结束的结束标志,从而让测试框架“知道”在结束时进行断言。但这种方式侵入性比较强,对测试语句不友好且违背了 Given/When/Then 的三段式套路,就像回调地狱一样的道理,如果让 done() 充斥着测试那么代码也就变得混乱。

Promise 让爱 then() 到底

it('the data is peanut butter', () => {
  expect.assertions(1);
  return fetchData().then(data => {
    expect(data).toBe('peanut butter');
  });
});
expect(Promise.resolve('lemon')).resolves.toBe('lemon')

expect(Promise.reject(new Error('octopus'))).rejects.toThrow('octopus')

其实这种方式也好不到哪去,无非就是把 done() 方式换成了 then() 又一次充斥在整个 expect 当中,混乱了 When 和 Then 两种本该分开的时刻。但也有一个不错的点,可以通过 Promise 的 .resolve().reject() 方法使测试分别验证正常或异常的情况。

Async/Await 让异步变得同步

test('the data is peanut butter', async () => {
  expect.assertions(1);
  const data = await fetchData();
  expect(data).toBe('peanut butter');
});

Async/Await 语法糖在业务代码当中就特别好使了,好处不多说直接看得见:原本需要 done()then() 的地方都不再混乱,又一次回归到了正常的 Given/When/Then 三段式套路,让测试代码变得非常清晰易读。唯一需要注意的是, 额外的 expect.assertions(number) 其实是验证在测试期间所调用的断言数量,这在测试多层异步代码时很有用,以确保实际调用回调中的断言次数。

意犹未尽吗?更加Jest相关的内容可以查看这篇文章 Testing JavaScript with Jest ,与此同时具体的 API 可以参考 官方文档

未完待续……

## 单元测试基础

  • ### 单元测试与自动化的意义
  • ### 为什么选择 Jest
  • ### Jest 的基本用法
  • ### 该如何测试异步代码?

## Vue 单元测试

  • ### Vue 组件的渲染方式
  • ### Wrapper find() 方法与选择器
  • ### UI 组件交互行为的测试

## Vuex 单元测试

  • ### CQRS 与 Redux-like 架构
  • ### 如何对 Vuex 进行单元测试
  • ### Vue组件和Vuex store的交互

## Vue应用测试策略


Recommend

  • 43

    本文的目标 2.2 在Vue应用的单元测试中,对 Vuex store 该如何测试?如何测试与 Vue 组件之间的交互? // Given 一个有基本的UT知识和Vue组件单元测试经验的开发者:walking: // When 当他:walking:阅读和练习本文的...

  • 38

    React单元测试策略及落地 单元测试是现代软件开发最基本,也普遍落地不力的实践。市面关于React单元测试的文章,普遍停留在“可以如何写”和介绍工具的层面,既未回答“为何必须做单元测试”,也未回答“单元测试的最佳实践”两个关键问题...

  • 37

    本文的目标 Vue 项目中测试收益如何最大化,如何配置高性价比的测试策略,即什么地方最该花力气测试,什么地方又可以暂且放一放? // Given 一个具备UT基础但找不到着力点的求索之徒:monkey: // When 当...

  • 18

    作者:宋成林 众所周知,单元测试功能,是组件库开发中必不可少的一部分,负责进行检查和验证,保证了组件的合理性和规范性。本文主要讲的就是单元测试在NUTUI 组件库 中的探索实践,我们将从如何编写单元测试、持续集成服务、C...

  • 2

    本文的目标 在 Vue 项目中如何推动整个团队循序渐进地采取单元测试策略?逐步提高代码质量和测试覆盖率? // Given 一个需要在团队中推行测试策略的 Tech Lead👨‍💻‍ // When 当他 🚶 阅读本文的 Vue 应用测试策略...

  • 9

    本文的目标 2.1 在 Vue 应用的单元测试中,对不同 UI 组件的单元测试有何不同?颗粒度该细到什么样的程度? // Given 一个有基本的 UT 知识但没写过 Vue 测试的新人 🚶 // When 当他 🚶 阅读和练习本文的 Vue 单元测试的部分...

  • 5

    Vue 应用单元测试的策略与实践 01本文主要尝试解决三个问题: 在 TDD 做完 Tasking 列完实例化数据之后,完全没有 UT 基础不知道该怎么写单元测试? 在 Vue 应用的单元测试中,对 UI 组件和 vuex store 等测试的区别有何不同?颗...

  • 9

    这是Go语言单元测试从零到溜系列教程的第0篇,主要讲解在Go语言中如何做单元测试以及介绍了表格驱动测试、回归测试,并且介绍了常用的断言工具。 Go语言单元测试从零到溜系列共7篇,本文是第0篇,介绍了Go语言单元测试的基础内容。本篇部分内容基于我之...

  • 8
    • developer.51cto.com 2 years ago
    • Cache

    Go应用单元测试实践

    高德打车运营的应用大多基于go进行开发的,我们希望在预集成环境下,当研发部署完代码,能自动触发单元测试和接口自动化测试,并生成覆盖率报告。参考了许多篇关于go单元测试的文章,有的缺少行增量覆盖率,有的缺少case运行结果/case运行日志。本文旨在...

  • 1

    1.0 前言 ​ 前面我们介绍了白盒测试方法,后面我们来介绍一下Junit 4,使用的是eclipse(用IDEA的小伙伴可以撤了) 1.1 配置Junit 4 1.1.1 安装包 我们需要三个jar包: ...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK