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

相关文章

Vue3 的 shallowRef 和 shallowReactive:优化性能

大家对 Vue3 的 ref 和 reactive 都很熟悉,那么对 shallowRef 和 shallowReactive 是否了解呢? 在编程和数据结构中,“shallow”(浅层)通常指对数据结构的最外层进行操作,而不递归地处理其内部或嵌套的数据。这种处理方式关注的是数据结构的第一层属性或元素,而忽略更深层次的嵌套内容。 1. 浅层与深层的对比 1.1 浅层(Shallow) 定义

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

Ilya-AI分享的他在OpenAI学习到的15个提示工程技巧

Ilya(不是本人,claude AI)在社交媒体上分享了他在OpenAI学习到的15个Prompt撰写技巧。 以下是详细的内容: 提示精确化:在编写提示时,力求表达清晰准确。清楚地阐述任务需求和概念定义至关重要。例:不用"分析文本",而用"判断这段话的情感倾向:积极、消极还是中性"。 快速迭代:善于快速连续调整提示。熟练的提示工程师能够灵活地进行多轮优化。例:从"总结文章"到"用

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template

性能测试介绍

性能测试是一种测试方法,旨在评估系统、应用程序或组件在现实场景中的性能表现和可靠性。它通常用于衡量系统在不同负载条件下的响应时间、吞吐量、资源利用率、稳定性和可扩展性等关键指标。 为什么要进行性能测试 通过性能测试,可以确定系统是否能够满足预期的性能要求,找出性能瓶颈和潜在的问题,并进行优化和调整。 发现性能瓶颈:性能测试可以帮助发现系统的性能瓶颈,即系统在高负载或高并发情况下可能出现的问题

中文分词jieba库的使用与实景应用(一)

知识星球:https://articles.zsxq.com/id_fxvgc803qmr2.html 目录 一.定义: 精确模式(默认模式): 全模式: 搜索引擎模式: paddle 模式(基于深度学习的分词模式): 二 自定义词典 三.文本解析   调整词出现的频率 四. 关键词提取 A. 基于TF-IDF算法的关键词提取 B. 基于TextRank算法的关键词提取

水位雨量在线监测系统概述及应用介绍

在当今社会,随着科技的飞速发展,各种智能监测系统已成为保障公共安全、促进资源管理和环境保护的重要工具。其中,水位雨量在线监测系统作为自然灾害预警、水资源管理及水利工程运行的关键技术,其重要性不言而喻。 一、水位雨量在线监测系统的基本原理 水位雨量在线监测系统主要由数据采集单元、数据传输网络、数据处理中心及用户终端四大部分构成,形成了一个完整的闭环系统。 数据采集单元:这是系统的“眼睛”,

【 html+css 绚丽Loading 】000046 三才归元阵

前言:哈喽,大家好,今天给大家分享html+css 绚丽Loading!并提供具体代码帮助大家深入理解,彻底掌握!创作不易,如果能帮助到大家或者给大家一些灵感和启发,欢迎收藏+关注哦 💕 目录 📚一、效果📚二、信息💡1.简介:💡2.外观描述:💡3.使用方式:💡4.战斗方式:💡5.提升:💡6.传说: 📚三、源代码,上代码,可以直接复制使用🎥效果🗂️目录✍️

Hadoop企业开发案例调优场景

需求 (1)需求:从1G数据中,统计每个单词出现次数。服务器3台,每台配置4G内存,4核CPU,4线程。 (2)需求分析: 1G / 128m = 8个MapTask;1个ReduceTask;1个mrAppMaster 平均每个节点运行10个 / 3台 ≈ 3个任务(4    3    3) HDFS参数调优 (1)修改:hadoop-env.sh export HDFS_NAMENOD

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06