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

相关文章

vite搭建vue3项目的搭建步骤

《vite搭建vue3项目的搭建步骤》本文主要介绍了vite搭建vue3项目的搭建步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学... 目录1.确保Nodejs环境2.使用vite-cli工具3.进入项目安装依赖1.确保Nodejs环境

Nginx搭建前端本地预览环境的完整步骤教学

《Nginx搭建前端本地预览环境的完整步骤教学》这篇文章主要为大家详细介绍了Nginx搭建前端本地预览环境的完整步骤教学,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录项目目录结构核心配置文件:nginx.conf脚本化操作:nginx.shnpm 脚本集成总结:对前端的意义很多

前端缓存策略的自解方案全解析

《前端缓存策略的自解方案全解析》缓存从来都是前端的一个痛点,很多前端搞不清楚缓存到底是何物,:本文主要介绍前端缓存的自解方案,文中通过代码介绍的非常详细,需要的朋友可以参考下... 目录一、为什么“清缓存”成了技术圈的梗二、先给缓存“把个脉”:浏览器到底缓存了谁?三、设计思路:把“发版”做成“自愈”四、代码

通过React实现页面的无限滚动效果

《通过React实现页面的无限滚动效果》今天我们来聊聊无限滚动这个现代Web开发中不可或缺的技术,无论你是刷微博、逛知乎还是看脚本,无限滚动都已经渗透到我们日常的浏览体验中,那么,如何优雅地实现它呢?... 目录1. 早期的解决方案2. 交叉观察者:IntersectionObserver2.1 Inter

Vue3视频播放组件 vue3-video-play使用方式

《Vue3视频播放组件vue3-video-play使用方式》vue3-video-play是Vue3的视频播放组件,基于原生video标签开发,支持MP4和HLS流,提供全局/局部引入方式,可监听... 目录一、安装二、全局引入三、局部引入四、基本使用五、事件监听六、播放 HLS 流七、更多功能总结在 v

利用Python操作Word文档页码的实际应用

《利用Python操作Word文档页码的实际应用》在撰写长篇文档时,经常需要将文档分成多个节,每个节都需要单独的页码,下面:本文主要介绍利用Python操作Word文档页码的相关资料,文中通过代码... 目录需求:文档详情:要求:该程序的功能是:总结需求:一次性处理24个文档的页码。文档详情:1、每个

JS纯前端实现浏览器语音播报、朗读功能的完整代码

《JS纯前端实现浏览器语音播报、朗读功能的完整代码》在现代互联网的发展中,语音技术正逐渐成为改变用户体验的重要一环,下面:本文主要介绍JS纯前端实现浏览器语音播报、朗读功能的相关资料,文中通过代码... 目录一、朗读单条文本:① 语音自选参数,按钮控制语音:② 效果图:二、朗读多条文本:① 语音有默认值:②

vue监听属性watch的用法及使用场景详解

《vue监听属性watch的用法及使用场景详解》watch是vue中常用的监听器,它主要用于侦听数据的变化,在数据发生变化的时候执行一些操作,:本文主要介绍vue监听属性watch的用法及使用场景... 目录1. 监听属性 watch2. 常规用法3. 监听对象和route变化4. 使用场景附Watch 的

前端导出Excel文件出现乱码或文件损坏问题的解决办法

《前端导出Excel文件出现乱码或文件损坏问题的解决办法》在现代网页应用程序中,前端有时需要与后端进行数据交互,包括下载文件,:本文主要介绍前端导出Excel文件出现乱码或文件损坏问题的解决办法,... 目录1. 检查后端返回的数据格式2. 前端正确处理二进制数据方案 1:直接下载(推荐)方案 2:手动构造

Vue实现路由守卫的示例代码

《Vue实现路由守卫的示例代码》Vue路由守卫是控制页面导航的钩子函数,主要用于鉴权、数据预加载等场景,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着... 目录一、概念二、类型三、实战一、概念路由守卫(Navigation Guards)本质上就是 在路