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

相关文章

在Ubuntu上部署SpringBoot应用的操作步骤

《在Ubuntu上部署SpringBoot应用的操作步骤》随着云计算和容器化技术的普及,Linux服务器已成为部署Web应用程序的主流平台之一,Java作为一种跨平台的编程语言,具有广泛的应用场景,本... 目录一、部署准备二、安装 Java 环境1. 安装 JDK2. 验证 Java 安装三、安装 mys

Python中构建终端应用界面利器Blessed模块的使用

《Python中构建终端应用界面利器Blessed模块的使用》Blessed库作为一个轻量级且功能强大的解决方案,开始在开发者中赢得口碑,今天,我们就一起来探索一下它是如何让终端UI开发变得轻松而高... 目录一、安装与配置:简单、快速、无障碍二、基本功能:从彩色文本到动态交互1. 显示基本内容2. 创建链

Node.js 中 http 模块的深度剖析与实战应用小结

《Node.js中http模块的深度剖析与实战应用小结》本文详细介绍了Node.js中的http模块,从创建HTTP服务器、处理请求与响应,到获取请求参数,每个环节都通过代码示例进行解析,旨在帮... 目录Node.js 中 http 模块的深度剖析与实战应用一、引言二、创建 HTTP 服务器:基石搭建(一

React实现原生APP切换效果

《React实现原生APP切换效果》最近需要使用Hybrid的方式开发一个APP,交互和原生APP相似并且需要IM通信,本文给大家介绍了使用React实现原生APP切换效果,文中通过代码示例讲解的非常... 目录背景需求概览技术栈实现步骤根据 react-router-dom 文档配置好路由添加过渡动画使用

java中VO PO DTO POJO BO DO对象的应用场景及使用方式

《java中VOPODTOPOJOBODO对象的应用场景及使用方式》文章介绍了Java开发中常用的几种对象类型及其应用场景,包括VO、PO、DTO、POJO、BO和DO等,并通过示例说明了它... 目录Java中VO PO DTO POJO BO DO对象的应用VO (View Object) - 视图对象

如何测试计算机的内存是否存在问题? 判断电脑内存故障的多种方法

《如何测试计算机的内存是否存在问题?判断电脑内存故障的多种方法》内存是电脑中非常重要的组件之一,如果内存出现故障,可能会导致电脑出现各种问题,如蓝屏、死机、程序崩溃等,如何判断内存是否出现故障呢?下... 如果你的电脑是崩溃、冻结还是不稳定,那么它的内存可能有问题。要进行检查,你可以使用Windows 11

Go信号处理如何优雅地关闭你的应用

《Go信号处理如何优雅地关闭你的应用》Go中的优雅关闭机制使得在应用程序接收到终止信号时,能够进行平滑的资源清理,通过使用context来管理goroutine的生命周期,结合signal... 目录1. 什么是信号处理?2. 如何优雅地关闭 Go 应用?3. 代码实现3.1 基本的信号捕获和优雅关闭3.2

正则表达式高级应用与性能优化记录

《正则表达式高级应用与性能优化记录》本文介绍了正则表达式的高级应用和性能优化技巧,包括文本拆分、合并、XML/HTML解析、数据分析、以及性能优化方法,通过这些技巧,可以更高效地利用正则表达式进行复杂... 目录第6章:正则表达式的高级应用6.1 模式匹配与文本处理6.1.1 文本拆分6.1.2 文本合并6

python中的与时间相关的模块应用场景分析

《python中的与时间相关的模块应用场景分析》本文介绍了Python中与时间相关的几个重要模块:`time`、`datetime`、`calendar`、`timeit`、`pytz`和`dateu... 目录1. time 模块2. datetime 模块3. calendar 模块4. timeit

使用Vue.js报错:ReferenceError: “Vue is not defined“ 的原因与解决方案

《使用Vue.js报错:ReferenceError:“Vueisnotdefined“的原因与解决方案》在前端开发中,ReferenceError:Vueisnotdefined是一个常见... 目录一、错误描述二、错误成因分析三、解决方案1. 检查 vue.js 的引入方式2. 验证 npm 安装3.