Vue2 应用测试学习 04 - BDD 案例

2024-08-23 01:08

本文主要是介绍Vue2 应用测试学习 04 - BDD 案例,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

BDD 介绍

TDD 的问题

  • 由于是以单元测试为主,会导致做出来的东西和实际功能需求相偏离
  • 过于依赖被测试功能的实现逻辑导致测试代码和实现代码耦合太高难以维护

BDD 行为驱动开发

  • 不需要再面向实现细节设计测试,取而代之的是面向行为来测试
  • BDD 的核心是关注软件的功能测试,所以 BDD 更多的是结合集成测试进行

BDD 开发流程

  1. 开发人员和非开发人员一起讨论确认需求
  2. 以一种自动化的方式将需求建立起来,并确认是否一致
  3. 最后,实现每个文档示例描述的行为,并从自动化测试开始以指导代码的开发
  4. 功能验收

BDD 解决方案和流程

Cucumber

https://cucumber.io/

  1. 需求分析
  2. 使用 Gherkin 语法描述需求
  3. 将 Gherkin 描述的需求文档映射为自动化测试用例
  4. 编写代码以通过测试
  5. 功能验收

通常需求描述文档由产品编写。

BDD + TDD

  • 需求分析
  • 将需求映射为集成测试用例
    • 单元测试
    • 编写代码以通过单元测试
  • 验证集成测试
  • 功能验收

轻量级 BDD 方案

  • 需求分析
  • 将需求映射为测试用例
  • 编写代码以通过测试
  • 功能验收

TDD + BDD

  • 需求分析
  • TDD 测试驱动开发
    • 编写单元测试
    • 编写代码以使测试通过
  • 编写集成测试验证功能需求

BDD 的核心是关注功能需求是否正确,所以先写测试后写测试都可以,但是通常情况下先写测试有助于对需求的理解,从而朝着正确的目标前进。

Vue 中的 BDD 技术栈

  • Jest + Vue Test Utils
    • 可以做单元测试
    • 也可以做集成测试
  • Jest + Vue Testing Library
    • 只能做集成测试

配置测试环境

继续使用 TDD 案例创建的项目,配置集成测试环境。

  1. 约定将所有的功能测试模块文件放到 /tests/feature 目录中(feature:功能、特性)
  2. 配置 npm scripts 脚本运行功能测试,指定测试文件匹配规则
"scripts": {..."test:unit": "vue-cli-service test:unit","coverage": "vue-cli-service test:unit --coverage","test:feature": "vue-cli-service test:unit --testMatch **/tests/feature/**/*.spec.[jt]s?(x)"
},
  1. 可以修改 ESLint 配置忽略 Jest 代码监测
module.exports = {...overrides: [{files: ['**/__tests__/*.{j,t}s?(x)','**/tests/unit/**/*.spec.{j,t}s?(x)','**/tests/feature/**/*.spec.{j,t}s?(x)',],env: {jest: true,},},],
}

编写测试用例:

// tests\feature\TodoApp.spec.js
test('a', () => {console.log('Hello World')
})

运行测试:

npm run test:feature

PS:一般集成测试的测试用例编写需要一定时间,在这个过程中没必要实时(--watch)的运行测试,可以等测试用例编写完成后再运行测试。

需求分析及编写功能测试用例

可以通过 describe 将测试用例分组,以功能命名。

test 描述可以是给定的行为和结论。

例如:

// tests\feature\TodoApp.spec.js
describe('添加任务', () => {test('在输入框中输入内容按下回车,应该添加任务到列表中', () => {})test('添加任务成功后,输入框内容应该被清空', () => {})
})describe('删除任务', () => {test('点击任务项中的删除按钮,任务应该被删除', () => {})
})describe('切换所有任务的完成状态', () => {test('选中切换所有按钮,所有任务应该变成已完成', () => {})test('取消选中切换所有按钮,所有任务应该变成未完成', () => {})test('当所有任务已完成的时候,全选按钮应该被选中,否则不选中', () => {})
})

下面开始编写实现细节。

添加任务到列表中

// tests\feature\TodoApp.spec.js
import { mount, createLocalVue } from '@vue/test-utils'
import VueRouter from 'vue-router'
import TodoApp from '@/components/TodoApp'const linkActiveClass = 'selected'
const localVue = createLocalVue()
localVue.use(VueRouter)
const router = new VueRouter({linkActiveClass,
})/** @type {import('@vue/test-utils').Wrapper} */
let wrapper = nullbeforeEach(() => {wrapper = mount(TodoApp, {localVue,router,})
})describe('添加任务', () => {test('在输入框中输入内容按下回车,应该添加任务到列表中', async () => {// 获取输入框const input = wrapper.findComponent('input[data-testid="new-todo"]')// 输入内容const text = 'Hello World'await input.setValue(text)// 按下回车await input.trigger('keyup.enter')// 断言:内容被添加到列表中expect(wrapper.findComponent('[data-testid="todo-item"]')).toBeTruthy()expect(wrapper.findComponent('[data-testid="todo-text"]').text()).toBe(text)})})

从该测试用例的实现可以看到,集成测试只关注功能,不关注内部怎么实现,例如组件细节、自定义事件名称和参数等。

添加任务完成清空文本框

test('添加任务成功后,输入框内容应该被清空', async () => {// 获取输入框const input = wrapper.findComponent('input[data-testid="new-todo"]')// 输入内容const text = 'Hello World'await input.setValue(text)// 按下回车await input.trigger('keyup.enter')// 断言:内容被添加到列表中expect(input.element.value).toBeFalsy()
})

删除单个任务项功能测试

describe('删除任务', () => {test('点击任务项中的删除按钮,任务应该被删除', async () => {// 准备测试环境数据await wrapper.setData({todos: [{ id: 1, text: 'eat', done: false },],})const todoItem = wrapper.findComponent('[data-testid="todo-item"]')// 断言:删除之前任务项是存在的expect(todoItem.exists()).toBeTruthy()// 找到任务项的删除按钮const delButton = wrapper.findComponent('[data-testid="delete"]')// 点击删除按钮await delButton.trigger('click')// 断言:删除按钮所在的任务项应该被移除expect(todoItem.exists()).toBeFalsy()})
})

切换单个的任务完成状态

describe('切换单个任务的完成状态', () => {test('选中任务完成状态按钮,任务的样式变成已完成状态', async () => {await wrapper.setData({todos: [{ id: 1, text: 'play', done: false },],})const todoDone = wrapper.findComponent('[data-testid="todo-done"]')const todoItem = wrapper.findComponent('[data-testid="todo-item"]')// 断言:初始未选中expect(todoDone.element.checked).toBeFalsy()// 断言:初始没有完成样式expect(todoItem.classes('completed')).toBeFalsy()// 选中任务项的复选框await todoDone.setChecked()// 断言结果expect(todoDone.element.checked).toBeTruthy()expect(todoItem.classes('completed')).toBeTruthy()})test('取消选中任务完成状态按钮,任务的样式变成未完成状态', async () => {await wrapper.setData({todos: [{ id: 1, text: 'play', done: true },],})const todoDone = wrapper.findComponent('[data-testid="todo-done"]')const todoItem = wrapper.findComponent('[data-testid="todo-item"]')expect(todoDone.element.checked).toBeTruthy()expect(todoItem.classes('completed')).toBeTruthy()await todoDone.setChecked(false)expect(todoDone.element.checked).toBeFalsy()expect(todoItem.classes('completed')).toBeFalsy()})
})

切换所有任务完成状态

describe('切换所有任务的完成状态', () => {test('选中切换所有按钮,所有任务应该变成已完成', async () => {await wrapper.setData({todos: [{ id: 1, text: 'play', done: false },{ id: 2, text: 'eat', done: false },{ id: 3, text: 'sleep', done: true },],})const toggleAll = wrapper.findComponent('[data-testid="toggle-all"]')const todoDones = wrapper.findAllComponents('[data-testid="todo-done"]')expect(toggleAll.element.checked).toBeFalsy()await toggleAll.setChecked()expect(toggleAll.element.checked).toBeTruthy()// 注意:todoDones 不是真正的数组,不能用 forEach 遍历for (let i = 0; i < todoDones.length; i++) {expect(todoDones.at(i).element.checked).toBeTruthy()}})test('取消选中切换所有按钮,所有任务应该变成未完成', async () => {await wrapper.setData({todos: [{ id: 1, text: 'play', done: true },{ id: 2, text: 'eat', done: true },{ id: 3, text: 'sleep', done: true },],})const toggleAll = wrapper.findComponent('[data-testid="toggle-all"]')const todoDones = wrapper.findAllComponents('[data-testid="todo-done"]')expect(toggleAll.element.checked).toBeTruthy()await toggleAll.setChecked(false)expect(toggleAll.element.checked).toBeFalsy()for (let i = 0; i < todoDones.length; i++) {expect(todoDones.at(i).element.checked).toBeTruthy()}})test('当所有任务已完成的时候,全选按钮应该被选中,否则不选中', async () => {await wrapper.setData({todos: [{ id: 1, text: 'play', done: true },{ id: 2, text: 'eat', done: false },{ id: 3, text: 'sleep', done: false },],})const toggleAll = wrapper.findComponent('[data-testid="toggle-all"]')// 注意:findAll 已废弃,未来版本将移除,官方推荐的 findAllComponents 当前项目使用的版本还不支持 CSS Selectorconst todoDones = wrapper.findAll('[data-testid="todo-done"]')expect(toggleAll.element.checked).toBeFalsy()for (let i = 0; i < todoDones.length; i++) {todoDones.at(i).setChecked()}await wrapper.vm.$nextTick()expect(toggleAll.element.checked).toBeTruthy()// 取消选中任意任务项await todoDones.at(0).setChecked(false)// 断言:全选应该取消选中expect(toggleAll.element.checked).toBeFalsy()})
})

编辑任务功能测试

describe('编辑任务', () => {beforeEach(async () => {await wrapper.setData({todos: [{ id: 1, text: 'play', done: false },],})})test('双击任务项文本,应该获得编辑状态', async () => {const todoText = wrapper.findComponent('[data-testid="todo-text"]')const todoItem = wrapper.findComponent('[data-testid="todo-item"]')// 双击之前确认任务项不是编辑状态expect(todoItem.classes('editing')).toBeFalsy()// 双击任务项文本await todoText.trigger('dblclick')// 双击之后,任务项应该获得编辑状态expect(todoItem.classes('editing')).toBeTruthy()})test('修改任务项文本按下回车后,应该保存修改以及取消编辑状态', async () => {const todoText = wrapper.findComponent('[data-testid="todo-text"]')const todoItem = wrapper.findComponent('[data-testid="todo-item"]')const todoEdit = wrapper.findComponent('[data-testid="todo-edit"]')// 双击文本获得编辑状态await todoText.trigger('dblclick')// 修改任务项文本const text = 'hello'await todoEdit.setValue(text)// 回车保存await todoEdit.trigger('keyup.enter')// 断言:任务项文本被修改expect(todoText.text()).toBe(text)// 断言:任务项的编辑状态取消了expect(todoItem.classes('editing')).toBeFalsy()})test('清空任务项文本,保存编辑应该删除任务项', async () => {const todoText = wrapper.findComponent('[data-testid="todo-text"]')const todoItem = wrapper.findComponent('[data-testid="todo-item"]')const todoEdit = wrapper.findComponent('[data-testid="todo-edit"]')// 双击文本获得编辑状态await todoText.trigger('dblclick')// 清空任务项文本await todoEdit.setValue('')// 回车保存await todoEdit.trigger('keyup.enter')// 断言:任务项应该被删除expect(todoItem.exists()).toBeFalsy()})test('修改任务项文本按下 ESC 后,应该取消编辑状态以及任务项文本保持不变', async () => {const todoText = wrapper.findComponent('[data-testid="todo-text"]')const todoItem = wrapper.findComponent('[data-testid="todo-item"]')const todoEdit = wrapper.findComponent('[data-testid="todo-edit"]')// 双击文本获得编辑状态await todoText.trigger('dblclick')// 获取原始内容const originText = todoText.text()// 修改任务项文本await todoEdit.setValue('hello')// ESC 取消await todoEdit.trigger('keyup.esc')// 断言:任务项还在expect(todoItem.exists()).toBeTruthy()// 断言:任务项文本不变expect(todoText.text()).toBe(originText)// 断言:任务项的编辑状态取消了expect(todoItem.classes('editing')).toBeFalsy()})
})

清除所有已完成任务项

describe('删除所有已完成任务', () => {test('如果所有任务已完成,清除按钮应该展示,否则不展示', async () => {await wrapper.setData({todos: [{ id: 1, text: 'play', done: false },{ id: 2, text: 'eat', done: false },{ id: 3, text: 'sleep', done: false },],})const clearCompleted = wrapper.findComponent('[data-testid="clear-completed"]')expect(clearCompleted.exists()).toBeFalsy()const todoDones = wrapper.findAll('[data-testid="todo-done"]')// 设置某个任务变成完成状态await todoDones.at(0).setChecked()await wrapper.vm.$nextTick()// 断言:清除按钮应该是展示状态// 注意:使用 `exists()` 时使用已获取的 Wrapper,如果 DOM 状态发生变化,`exists()` 可能不会跟着变化,建议重新获取 Wrapperexpect(wrapper.findComponent('[data-testid="clear-completed"]').exists()).toBeTruthy()})test('点击清除按钮,应该删除所有已完成任务', async () => {await wrapper.setData({todos: [{ id: 1, text: 'play', done: true },{ id: 2, text: 'eat', done: false },{ id: 3, text: 'sleep', done: true },],})const clearComplelted = wrapper.findComponent('[data-testid="clear-completed"]')// 点击清除按钮await clearComplelted.trigger('click')const todoItems = wrapper.findAll('[data-testid="todo-item"]')expect(todoItems.length).toBe(1)expect(todoItems.at(0).text()).toBe('eat')expect(wrapper.findComponent('[data-testid="clear-completed"]').exists()).toBeFalsy()})
})

展示所有未完成任务数量

describe('展示所有未完成任务数量', () => {test('展示所有未完成任务数量', async () => {await wrapper.setData({todos: [{ id: 1, text: 'play', done: true },{ id: 2, text: 'eat', done: true },{ id: 3, text: 'sleep', done: true },],})const getDoneTodosCount = () => {const dones = wrapper.findAll('[data-testid="todo-done"]')let count = 0for (let i = 0; i < dones.length; i++) {if (!dones.at(i).element.checked) {count++}}return count}// 断言未完成任务的数量const todoDonesCount = wrapper.findComponent('[data-testid="done-todos-count"]')expect(todoDonesCount.text()).toBe(getDoneTodosCount().toString())// 切换一个任务的状态,再断言const dones = wrapper.findAll('[data-testid="todo-done"]')await dones.at(0).setChecked(false)expect(todoDonesCount.text()).toBe(getDoneTodosCount().toString())// 删除任务项,再断言await wrapper.findComponent('[data-testid="delete"]').trigger('click')expect(todoDonesCount.text()).toBe(getDoneTodosCount().toString())})
})

数据筛选功能测试

给导航链接添加 data-testid

<ul class="filters"><li><router-link to="/" data-testid="link-all" exact>All</router-link></li><li><router-link to="/active" data-testid="link-active">Active</router-link></li><li><router-link to="/completed" data-testid="link-completed">Completed</router-link></li>
</ul>
describe('数据筛选', () => {const todos = [{ id: 1, text: 'play', done: true },{ id: 2, text: 'eat', done: false },{ id: 3, text: 'sleep', done: true },]const filterTodos = {all: () => todos,active: () => todos.filter(t => !t.done),completed: () => todos.filter(t => t.done),}beforeEach(async () => {await wrapper.setData({todos,})})test('点击 all 链接,应该展示所有任务,并且 all 链接应该高亮', async () => {// vue router 跳转重复导航时会返回 rejected Promise,这里捕获一下避免命令行中显示错误提示router.push('/').catch(() => {})// 路由导航后要等待视图更新await wrapper.vm.$nextTick()expect(wrapper.findAll('[data-testid="todo-item"]').length).toBe(filterTodos.all().length)expect(wrapper.findComponent('[data-testid="link-all"]').classes()).toContain(linkActiveClass)})test('点击 active 链接,应该展示所有未完成任务,并且 active 链接应该高亮', async () => {router.push('/active').catch(() => {})await wrapper.vm.$nextTick()expect(wrapper.findAll('[data-testid="todo-item"]').length).toBe(filterTodos.active().length)expect(wrapper.findComponent('[data-testid="link-active"]').classes()).toContain(linkActiveClass)})test('点击 completed 链接,应该展示所有已完成任务,并且 completed 链接应该高亮', async () => {router.push('/completed').catch(() => {})await wrapper.vm.$nextTick()expect(wrapper.findAll('[data-testid="todo-item"]').length).toBe(filterTodos.completed().length)expect(wrapper.findComponent('[data-testid="link-completed"]').classes()).toContain(linkActiveClass)})
})

优化获取 data-testid 的方法

增加获取 Wrapper 的实例方法

beforeEach(() => {wrapper = mount(TodoApp, {localVue,router,})// 增加通过 data-testid 获取 Wrapper 的方法wrapper.findById = id => {return wrapper.findComponent(`[data-testid="${id}"]`)}wrapper.findAllById = id => {return wrapper.findAll(`[data-testid="${id}"]`)}
})// 示例:
describe('添加任务', () => {test('在输入框中输入内容按下回车,应该添加任务到列表中', async () => {// 获取输入框// const input = wrapper.findComponent('input[data-testid="new-todo"]')const input = wrapper.findById('new-todo')// 输入内容const text = 'Hello World'await input.setValue(text)// 按下回车await input.trigger('keyup.enter')// 断言:内容被添加到列表中// expect(wrapper.findComponent('[data-testid="todo-item"]')).toBeTruthy()// expect(wrapper.findComponent('[data-testid="todo-text"]').text()).toBe(text)expect(wrapper.findById('todo-item')).toBeTruthy()expect(wrapper.findById('todo-text').text()).toBe(text)})
})

全局使用

官方文档:Configuring Jest

当前只是在当前测试文件中添加了方法,要想在全局使用,可以让代码在运行每个测试文件之前执行,通过在 Jest 的配置文件中配置 setupFilessetupFilesAfterEnv

它们的作用是指定在运行每个测试文件之前执行的代码文件,两者的区别只是 setupFiles 中不能编写测试用例(例如不能使用 testexpect等 API),而 setupFilesAfterEnv 可以,

// jest.config.js
module.exports = {preset: '@vue/cli-plugin-unit-jest',setupFilesAfterEnv: ['./jest.setup.js'],
}

新建 setup 文件添加实例方法:

// jest.setup.js
import { Wrapper } from '@vue/test-utils'// 1. 通过 Wrapper 原型添加实例方法
// 2. 使用 function 而不是箭头函数,保证 this 指向
Wrapper.prototype.findById = function (id) {return this.findComponent(`[data-testid="${id}"]`)
}Wrapper.prototype.findAllById = function (id) {return this.findAll(`[data-testid="${id}"]`)
}

注释掉测试文件中添加实例方法的代码,重新运行测试。

这篇关于Vue2 应用测试学习 04 - BDD 案例的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1097917

相关文章

Python将博客内容html导出为Markdown格式

《Python将博客内容html导出为Markdown格式》Python将博客内容html导出为Markdown格式,通过博客url地址抓取文章,分析并提取出文章标题和内容,将内容构建成html,再转... 目录一、为什么要搞?二、准备如何搞?三、说搞咱就搞!抓取文章提取内容构建html转存markdown

在React中引入Tailwind CSS的完整指南

《在React中引入TailwindCSS的完整指南》在现代前端开发中,使用UI库可以显著提高开发效率,TailwindCSS是一个功能类优先的CSS框架,本文将详细介绍如何在Reac... 目录前言一、Tailwind css 简介二、创建 React 项目使用 Create React App 创建项目

vue使用docxtemplater导出word

《vue使用docxtemplater导出word》docxtemplater是一种邮件合并工具,以编程方式使用并处理条件、循环,并且可以扩展以插入任何内容,下面我们来看看如何使用docxtempl... 目录docxtemplatervue使用docxtemplater导出word安装常用语法 封装导出方

springboot循环依赖问题案例代码及解决办法

《springboot循环依赖问题案例代码及解决办法》在SpringBoot中,如果两个或多个Bean之间存在循环依赖(即BeanA依赖BeanB,而BeanB又依赖BeanA),会导致Spring的... 目录1. 什么是循环依赖?2. 循环依赖的场景案例3. 解决循环依赖的常见方法方法 1:使用 @La

Python中随机休眠技术原理与应用详解

《Python中随机休眠技术原理与应用详解》在编程中,让程序暂停执行特定时间是常见需求,当需要引入不确定性时,随机休眠就成为关键技巧,下面我们就来看看Python中随机休眠技术的具体实现与应用吧... 目录引言一、实现原理与基础方法1.1 核心函数解析1.2 基础实现模板1.3 整数版实现二、典型应用场景2

Python Dash框架在数据可视化仪表板中的应用与实践记录

《PythonDash框架在数据可视化仪表板中的应用与实践记录》Python的PlotlyDash库提供了一种简便且强大的方式来构建和展示互动式数据仪表板,本篇文章将深入探讨如何使用Dash设计一... 目录python Dash框架在数据可视化仪表板中的应用与实践1. 什么是Plotly Dash?1.1

Android Kotlin 高阶函数详解及其在协程中的应用小结

《AndroidKotlin高阶函数详解及其在协程中的应用小结》高阶函数是Kotlin中的一个重要特性,它能够将函数作为一等公民(First-ClassCitizen),使得代码更加简洁、灵活和可... 目录1. 引言2. 什么是高阶函数?3. 高阶函数的基础用法3.1 传递函数作为参数3.2 Lambda

Vue中组件之间传值的六种方式(完整版)

《Vue中组件之间传值的六种方式(完整版)》组件是vue.js最强大的功能之一,而组件实例的作用域是相互独立的,这就意味着不同组件之间的数据无法相互引用,针对不同的使用场景,如何选择行之有效的通信方式... 目录前言方法一、props/$emit1.父组件向子组件传值2.子组件向父组件传值(通过事件形式)方

css中的 vertical-align与line-height作用详解

《css中的vertical-align与line-height作用详解》:本文主要介绍了CSS中的`vertical-align`和`line-height`属性,包括它们的作用、适用元素、属性值、常见使用场景、常见问题及解决方案,详细内容请阅读本文,希望能对你有所帮助... 目录vertical-ali

Java中&和&&以及|和||的区别、应用场景和代码示例

《Java中&和&&以及|和||的区别、应用场景和代码示例》:本文主要介绍Java中的逻辑运算符&、&&、|和||的区别,包括它们在布尔和整数类型上的应用,文中通过代码介绍的非常详细,需要的朋友可... 目录前言1. & 和 &&代码示例2. | 和 ||代码示例3. 为什么要使用 & 和 | 而不是总是使