前端自动化测试:TDD 和 BDD 哪个更好一些?

·  阅读 2002
前端自动化测试:TDD 和 BDD 哪个更好一些?

Vue 应用测试

​项目环境搭建

运行 vue create [project-name] 来创建一个新项目。选择 "Manually selectfeatures" 和 "UnitTesting",以及 "Jest" 作为 test runner。

一旦安装完成,cd 进入项目目录中并运行 yarn test:unit。

通过 jest 配置文件:

\jest.config.js ==> node_modules\@vue\cli-plugin-unit-jest\jest-preset.js ==> \node_modules\@vue\cli-plugin-unit-jest\presets\default\jest-preset.js

jest-preset.js 文件就是 Vue 项目创建后,默认的 jest 配置文件:

module.exports= {
  // 可加载模块的后缀名
  moduleFileExtensions: [
    'js',
    'jsx',
    'json',
    // tell Jest to handle *.vue files
    'vue'
  ],
  // 转换方式
  transform: {
    // process *.vue files with vue-jest
    // 如果.vue结尾的,使用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
  // webpack 的别名映射转换
  moduleNameMapper: {
    '^@/(.*)$':'<rootDir>/src/$1'
  },
  // 指定测试环境为 jsdom 
  testEnvironment:'jest-environment-jsdom-fifteen',

​  // serializer for snapshots
  // 快照序列化器
  // 使用 jest-serializer-vue 进行组件快照的序列化方式
  // 就是将组件转为字符串,后面进行快照测试时,就可以看到了
  snapshotSerializers: [
    'jest-serializer-vue'
  ],​

  // 测试代码文件在哪里
  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')
  ]
}
复制代码

快速体验

默认测试用例:tests\unit\example.spec.js

//tests\unit\example.spec.js
// 导入组件挂载器,不用手动写vue入口
import { shallowMount } from'@vue/test-utils'
// 导入要测试的组件
import HelloWorld from'@/components/HelloWorld.vue'​

describe('HelloWorld.vue', () => {
  it('rendersprops.msg when passed', () => {
    const msg ='newmessage'
    const wrapper =shallowMount(HelloWorld, {
      props: { msg }
    })
    expect(wrapper.text()).toMatch(msg)
  })
})



$ npm runtest:unit
复制代码

搭建完基本的 Vue 测试环境,在正式开始 Vue 测试之前,我们先了解一下测试开发的方法

测试开发方式

测试不仅能够验证软件功能、保证代码质量,也能够影响软件开发的模式

测试开发有两个流派:

  • TDD:测试驱动开发,先写测试后实现功能

  • BDD:行为驱动开发,先实现功能后写测试

什么是TDD

TDD(Test-driven development),就是测试驱动开发,是敏捷开发中的一项核心实践和技术,也是一种软件设计方法论。

它的原理就是在编写代码之前先编写测试用例,由测试来决定我们的代码。而且 TDD 更多地需要编写独立的测试用例,比如只测试一个组件的某个功能点,某个工具函数等。

TDD开发流程:

  • 编写测试用例

  • 运行测试

  • 编写代码使测试通过

  • 重构/优化代码

  • 新增功能,重复上述步骤

    // tests\unit\example.spec.js // 导入组件挂载器,不用手动写vue入口 import { shallowMount } from'@vue/test-utils' // 导入要测试的组件 import HelloWorld from'@/components/HelloWorld.vue'​

    import {add} from'@/utiles/math.js' // 输入:1,2 // 输出:3 test('sum', () => { expect(add(1,2)).toBe(3) })

单纯运行测试代码肯定报错,有了测试代码,为了通过测试,再具体写 math 模块中的 add() 方法:

// math.js
functionadd (a, b) {
  return a + b
}
exportdefault add
复制代码

Vue 3 的 TDD 测试用例

src\components\TodoHeader.vue 组件内容

<template>
  <header>
   <h1>Todos</h1>
    <input
     v-model="inputValue"
     placeholder="What needs to be done?"
     data-testid="todo-input"
     @keyup.enter="handleNewTodo"
    />
 </header>
</template>
复制代码

测试用例:

tests\unit\example.spec.js

// 导入组件挂载器,不用手动写vue入口
import { shallowMount } from'@vue/test-utils'
// 导入要测试的组件
import HelloWorld from'@/components/HelloWorld.vue'

​import TodoHeader from'@/components/TodoHeader.vue'
test('unit: new todo',async () => {
  const wrapper =shallowMount(TodoHeader) // 挂载渲染组件
  const input = wrapper.find('[data-testid="todo-input"]') // 查找input
  // 给input设置一个值
  const text ='helloworld'
  await input.setValue(text)
  // 触发事件
  await input.trigger('keyup.enter')
  // =========
  // 以上是具体操作,输入内容按下回车后,希望做什么?↓👇
  // =========

​  // 验证应该发布一个对外的 new-todo 的事件
  expect(wrapper.emitted()['new-todo']).toBeTruthy()
  // 验证导出事件的参数是否是传入的值
  expect(wrapper.emitted()['new-todo'][0][0]).toBe(text)
  // 验证文本框内容是否清空
  expect(input.element.value).toBe('')

​})
复制代码

src\components\TodoHeader.vue 组件内容

exportdefault {
  data(){
    return {
      inputValue:''
    }
  },
  methods:{
    handleNewTodo(){
      if(this.inputValue.trim().length){
        // 发布对外的 new-todo 事件值为文本框输入内容
        this.$emit('new-todo',this.inputValue)
        this.inputValue=''
      }
    }
  }
};
复制代码

​TDD 原则

  • 独立测试

不同代码的测试应该相互独立,一个类对应一个测试类(对于 C 代码或 C++ 全局函数,则一个文件对应一个测试文件),一个函数对应一个测试函数。

用例也应各自独立。每个用例不能使用其他用例的结果数据,结果也不能依赖于用例执行顺序。

一个角色:开发过程包含多种工作,如:编写测试代码、编写产品代码、代码重构等。做不同的工作时,应专注于当前的角色,不要过多考虑其他方面的细节。

  • 测试列表

代码的功能点可能很多,并且需求可能是陆续出现的,任何阶段想添加功能时,应把相关功能点加到测试列表中,然后才能继续手头工作,避免疏漏。

  • 测试驱动

即利用测试来驱动开发,是TDD的核心。要实现某个功能,要编写某个类或某个函数,应首先编写测试代码,明确这个类、这个函数如何使用,如何测试,然后在对其进行设计、编码。

  • 先写断言

编写测试代码时,应该首先编写判断代码功能的断言语句,然后编写必要的辅助语句。

  • 可测试性

产品代码设计、开发时应尽可能提高可测试性。每个代码单元的功能应该比较单纯,“各家自扫门前雪”,每个类、每个函数应该只做它该做的事,不要弄成大杂烩。尤其是增加新功能时,不要为了图一时之便,随便在原有代码中添加功能,对于 C++ 编程,应多考虑使用子类、继承、重载等OO方法。

  • 及时重构

对结构不合理,重复等“味道”不好的代码,在测试通过后,应及时进行重构。

  • 小步前进

软件开发是复杂性非常高的工作,小步前进是降低复杂性的好办法。

TDD 的优点

  • 保证代码质量,因为先编写测试,所以可能出现的问题都被提前发现了;

  • 促进开发人员思考,有利于程序的模块设计

  • 测试覆盖率高,因为后编写代码,因此测试用例基本都能照顾到;

TDD 的缺点

  • 代码量增多,大多数情况下测试代码是功能代码的两倍甚至更多;

  • 业务耦合度高,测试用例中使用了业务中一些模拟的数据,当业务代码变更的时候,要去重新组织测试用例;

  • 关注点过于独立,由于单元测试只关注这一个单元的健康状况,无法保证多个单元组成的整体是否正常;

个人理解在前端应用实际开发过程中 TDD 更适合开发纯函数库,比如 Lodash、Vue、React 等。

BDD

TDD 最大一个问题是在于开发人员最终做出来的东西和实际功能需求可能相偏离,为了解决这一问题有人发明了 BDD。BDD(Behavior-driven development)行为驱动开发,是测试驱动开发延伸出来的一种敏捷软件开发技术。

BDD 解决的另外一个关键问题就是如何定义 TDD 或单元测试过程中的细节。一些不良的单元测试的一个常见问题是**过于依赖被测试功能的实现逻辑。**这通常意味着如果你要修改实现逻辑,即使输入输出没有变,通常也需要去更新测试代码。这就造成了一个问题,让开发人员对测试代码的维护感觉乏味和厌烦。

BDD 核心目的是为了解决 TDD 模式下开发和实际功能需求不一致而诞生,BDD 不需要再面向实现细节设计测试,取而代之的是面向行为来测试。它是从产品角度出发,鼓励开发人员和非开发人员(产品、QA、客户等)之间的协作。由于 BDD 的核心是关注软件的功能测试,所以 BDD 更多的是结合集成测试进行,它是黑盒的。

BDD 的开发流程

1、开发人员和非开发人员一起讨论确认需求

2、以一种自动化的方式将需求建立起来,并确认是否一致

3、最后,实现每个文档示例描述的行为,并从自动化测试开始以指导代码的开发

这样做使得每一次的更改都较小并快速迭代,每次需要更多信息时都将其上移。每次自动化并实现一个新示例时,便为系统添加了一些有价值的内容,并准备响应反馈。

理想中的 BDD 解决方案最流行的是 Cucumber。它的协作流程是这样:

1、开发人员与产品、测试、客户等人员沟通确认需求

2、使用统一的 Gherkin 语法将功能需求转换为需求文档

用描述性自然语言定义的测试,客户、测试人员和开发人员都能看得懂,能达成共识,这种语法叫做 Gherkin Syntax,小黄瓜语法。

  • 以关键字 Scenario、Feature 等来描述场景

  • 以关键字 Given、When、Then 来描述步骤

    Feature: 添加任务

    ​ Scenario: 在输入框中输入任务名敲回车确定,输出到任务列表中 Given "Hello World" When 在输入框中敲回车 Then 任务列表增加一个名称为 "Hello World" 的任务

    ​ Scenario: 在输入框中输入空内容,不输出到任务列表中 Given "" When 在输入框中敲回车 Then 任务列表中不增加任何内容

Cucumber 读取使用 Gherkin 语法描述的纯文本形式的可执行规范,并验证该软件是否满足那些规范所说的内容。规范包含多个示例或方案。

每个方案都是 Cucumber 要执行的步骤的列表。Cucumber 验证软件是否符合规范,并生成一个报告,指出每种情况的成功或失败。

3、开发人员根据 Gherkin 编写测试用例

4、编写代码使测试通过

5、新增功能重复上述步骤

BDD + TDD

BDD 注重的是产品功能,可能无法保证很好的代码质量和测试覆盖率,所以还有人提出一种方案就是 BDD + TDD。

BDD

– 需求分析

– 描述需求定义文档

– 编写集成测试用例

TDD

– 编写单元测试用例

– 编写代码使单元测试通过

– 重构优化

编写代码使集成测试通过

增加功能重复上述步骤

个人理解也可以把 BDD 看作是在需求与 TDD 之间架起一座桥梁,它将需求进一步场景化,更具体地描述系统应该满足哪些行为和场景,让 TDD 的输入更优雅、更可靠。

还有一种更轻量的 BDD 方案就是以集成测试为主的开发方案。

  • 需求分析

  • 编写集成测试用例

  • 运行测试

  • 代码实现使测试通过

  • 重构优化

  • 增加功能重复上述步骤

BDD 的优点:

  • 由于侧重于需求功能的完整度,所以能给开发人员增加更多对程序的信心

  • 由于仅关注功能,不关注实现细节,有利于测试代码和实际代码解耦

  • 由于大多数为编写集成测试,相比 TDD 有更好的开发效率

BDD 的缺点:

  • 因为以功能性的集成测试为主,因此不是那么关注每个函数功能,测试覆盖率比较低

  • 没有 TDD 那么严格的保证代码质量

TDD vs BDD

个人推荐:

  • 建议开发功能函数库使用 TDD 方案

  • 建议开发业务系统使用 BDD 方案

俗话说生活就像一场旅程,不必在乎目的地。虽然对生活来说这可能是正确的,但对开发应用程序来说却恰恰相反,你可以选择单独使用其中一种方法,也可以综合使用这几个方法以取得更好的效果。只要是编写可节省时间的、有价值的测试就可以,如何编写无关紧要。

前端自动化测试的权衡利弊

当我们开始编写自动化测试时,可能想要测试所有的东西。每个搬砖仔可能都体会过未经测试的应用程序带来的痛苦,但在测试过程中,很快我又学到了另一课——测试会减缓开发速度

在编写测试时,请务必牢记编写测试的目的。通常,测试的目的是为了节省时间。如果你正在进行的项目是稳定的并且会长期开发,那么测试是可以带来收益的。

但是如果测试编写与维护的时间长于它们可以节省的时间,那么你根本不应该编写测试。当然,在编写代码之前你很难知道通过测试可以节省多少时间。但是,假设你正在一个短期项目中创建原型,或者是在一个创业公司迭代一个想法,那你可能不会从编写测试中获得收益。

凡事都有两面性,软件测试也不是银弹,好处虽然明显,却并不是所有的项目都值得引入测试框架,毕竟维护测试用例也是需要成本的。对于一些需求频繁变更、复用性较低的内容,比如活动页面,让开发专门抽出人力来写测试用例确实得不偿失。

而适合引入测试场景大概有这么几个:

  • 需要长期维护的项目。它们需要测试来保障代码可维护性、功能的稳定性。

  • 较为稳定的项目、或项目中较为稳定的部分。给它们写测试用例,维护成本低。

  • 被多次复用的部分,比如一些通用组件和库函数。因为多处复用,更要保障质量。

测试确实会带给我们相当多的好处,但不是立刻就能够体会到。测试就像保险,如果身体健康顺顺利利,几十年可能都用不上。测试也一样,写了可以买个放心,这就是对代码的一种保障,有 bug 可以尽快测出来,没 bug 最好,但不能说“写那么多测试,结果测不出 bug,是浪费时间”。

Over

分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改