案例介绍

下面使用 TodoMVC 提供的开源 Todos List 任务清单示例,学习使用 TDD 方式开发 Vue 应用。

案例演示效果:Vue.js • TodoMVC

基本功能包括:

  • 输入任务内容,点击回车,任务被添加到列表中
  • 切换单个或所有任务完成状态
  • 删除任务
  • 显示未完成任务数量
  • 删除所有已完成的任务
  • 数据筛选
  • 修改任务内容(回车保存修改,ESC取消编辑,内容为空则删除任务)

案例创建

创建组件文件 src\components\TodoApp\index.vue,从 Github 仓库的 index.html 中拷贝页面内容:

<!-- src\components\TodoApp\index.vue -->
<template><section class="todoapp"><header class="header"><h1>todos</h1><input class="new-todo" placeholder="What needs to be done?" autofocus /></header><section class="main"><input id="toggle-all" class="toggle-all" type="checkbox" /><label for="toggle-all">Mark all as complete</label><ul class="todo-list"><!-- These are here just to show the structure of the list items --><!-- List items should get the class `editing` when editing and `completed` when marked as completed --><li class="completed"><div class="view"><input class="toggle" type="checkbox" checked /><label>Taste JavaScript</label><button class="destroy"></button></div><input class="edit" value="Create a TodoMVC template" /></li><li><div class="view"><input class="toggle" type="checkbox" /><label>Buy a unicorn</label><button class="destroy"></button></div><input class="edit" value="Rule the web" /></li></ul></section><!-- This footer should be hidden by default and shown when there are todos --><footer class="footer"><!-- This should be `0 items left` by default --><span class="todo-count"><strong>0</strong> item left</span><!-- Remove this if you don't implement routing --><ul class="filters"><li><a class="selected" href="#/">All</a></li><li><a href="#/active">Active</a></li><li><a href="#/completed">Completed</a></li></ul><!-- Hidden if no completed items are left ↓ --><button class="clear-completed">Clear completed</button></footer></section>
</template>

在 App.vue 中引入组件:

<template><div id="app"><TodoApp /></div>
</template><script>
import TodoApp from '@/components/TodoApp'export default {name: 'App',components: { TodoApp }
}
</script>

从 package.json 中找到样式依赖 todomvc-app-css,复制 index.css 到项目中:新建 src\style\index.css 文件。

main.js 中引入:

// src\main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import './style/index.css'Vue.config.productionTip = falsenew Vue({router,store,render: h => h(App)
}).$mount('#app')

npm run serve启动应用,查看效果 http://localhost:8080/

划分组件单元

下面按照 TDD(测试驱动开发) 方式开发应用,首先将功能按照组件拆分。

TodoHeader

<!-- src\components\TodoApp\TodoHeader.vue -->
<template><header class="header"><h1>todos</h1><input class="new-todo" placeholder="What needs to be done?" autofocus /></header>
</template><script>
export default {name: 'TodoHeader'
}
</script>

TodoFooter

<!-- src\components\TodoApp\TodoFooter.vue -->
<template><!-- This footer should be hidden by default and shown when there are todos --><footer class="footer"><!-- This should be `0 items left` by default --><span class="todo-count"><strong>0</strong> item left</span><!-- Remove this if you don't implement routing --><ul class="filters"><li><a class="selected" href="#/">All</a></li><li><a href="#/active">Active</a></li><li><a href="#/completed">Completed</a></li></ul><!-- Hidden if no completed items are left ↓ --><button class="clear-completed">Clear completed</button></footer>
</template><script>
export default {name: 'TodoFooter'
}
</script>

TodoItem

<!-- src\components\TodoApp\TodoItem.vue -->
<template><li><!-- <li class="completed"> --><div class="view"><input class="toggle" type="checkbox" /><!-- <input class="toggle" type="checkbox" checked /> --><label>Taste JavaScript</label><button class="destroy"></button></div><input class="edit" value="Create a TodoMVC template" /></li>
</template><script>
export default {name: 'TodoItem'
}
</script>

TodoApp

<!-- src\components\TodoApp\index.vue -->
<template><section class="todoapp"><TodoHeader /><section class="main"><input id="toggle-all" class="toggle-all" type="checkbox" /><label for="toggle-all">Mark all as complete</label><ul class="todo-list"><!-- These are here just to show the structure of the list items --><!-- List items should get the class `editing` when editing and `completed` when marked as completed --><TodoItem /><TodoItem /></ul></section><!-- This footer should be hidden by default and shown when there are todos --><TodoFooter /></section>
</template><script>
import TodoHeader from './TodoHeader'
import TodoFooter from './TodoFooter'
import TodoItem from './TodoItem'export default {name: 'TodoApp',components: { TodoHeader, TodoFooter, TodoItem }
}
</script>

TodoHeader 组件

TodoHeader 组件的功能是在文本框中输入内容,按下回车应该发送一个自定义事件,自定义事件传递一个字符串,并在最后清空文本框。

编写测试用例

首先编写测试用例,文件位置推荐就近原则。

在 TodoApp 组件文件夹下创建测试用例文件夹,文件夹名为 __tests__,Vue Test Utils 会查找这这个目录下的文件,作为测试文件执行。

测试文件最好以测试的组件名命名:TodoHeader.js

// src\components\TodoApp\__tests__\TodoHeader.js
import { shallowMount } from '@vue/test-utils'
import TodoHeader from '@/components/TodoApp/TodoHeader'describe('TodoHeader.vue', () => {test('New todo', async () => {const wrapper = shallowMount(TodoHeader)// 可以给元素添加一个专门用于测试的 `data-testid`,方便测试的时候获取这个元素const input = wrapper.findComponent('input[data-testid="new-todo"]')const text = 'play'// 文本框填入内容// 操作视图也建议使用 await 等待一下// 因为它可能会修改 vm 实例的状态,这样更稳妥一些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('')})
})

使用监视模式运行测试(结果当然是失败):

# 监视并运行全部测试文件
npm run test:unit -- --watch
# or 监视并运行指定测试文件
npm run test:unit -- TodoHeader.js --watch

完善组件功能

接着根据测试用例完善组件功能:

<!-- src\components\TodoApp\TodoHeader.vue -->
<template><header class="header"><h1>todos</h1><inputclass="new-todo"placeholder="What needs to be done?"autofocusdata-testid="new-todo"@keyup.enter="handleNewTodo"/></header>
</template><script>
export default {name: 'TodoHeader',methods: {handleNewTodo (e) {const value = e.target.value.trim()if (!value.length) {return}this.$emit('new-todo', value)e.target.value = ''}}
}
</script>

可以发现,除非调试样式,整个过程完全不需要打开浏览器,就可以完成这个组件的功能开发,并且大多数情况下,开发出来的功能一定是正确的。

在组件开发完成并通过测试后,可以将代码进行 git 提交,这样重新运行测试命令watch 模式下,文件没有变更,就不再会对它进行测试了。

TodoApp 组件

TodoApp 作为父组件应该接收 TodoHeader 发出的自定义事件,然后通过一个函数来处理它: TodoApp 应该管理一个数组,可以向这个数组添加任务项。

编写测试用例

// src\components\TodoApp\__tests__\TodoApp.js
import { shallowMount } from '@vue/test-utils'
import TodoApp from '@/components/TodoApp'
import TodoItem from '@/components/TodoApp/TodoItem'describe('TodoApp.vue', () => {test('New todo', async () => {const wrapper = shallowMount(TodoApp)const text = 'play'// 调用组件的方法,添加任务项wrapper.vm.handleNewTodo(text)// 期望管理的数组中包含刚添加的任务项const todo = wrapper.vm.todos.find(t => t.text === text)expect(todo).toBeTruthy()})test('Todo List', async () => {const wrapper = shallowMount(TodoApp)// 初始化默认数据,并等待视图更新const todos = [{ id: 1, text: 'eat', done: false },{ id: 2, text: 'play', done: true },{ id: 3, text: 'sleep', done: false }]await wrapper.setData({todos})// 期望指定子组件被渲染了3个expect(wrapper.findAllComponents(TodoItem).length).toBe(todos.length)})
})

完善组件功能

<!-- src\components\TodoApp\index.vue -->
<template><section class="todoapp"><TodoHeader @new-todo="handleNewTodo" /><section class="main"><input id="toggle-all" class="toggle-all" type="checkbox" /><label for="toggle-all">Mark all as complete</label><ul class="todo-list"><!-- These are here just to show the structure of the list items --><!-- List items should get the class `editing` when editing and `completed` when marked as completed --><TodoItem v-for="todo in todos" :key="todo.id" /></ul></section><!-- This footer should be hidden by default and shown when there are todos --><TodoFooter /></section>
</template><script>
import TodoHeader from './TodoHeader'
import TodoFooter from './TodoFooter'
import TodoItem from './TodoItem'export default {name: 'TodoApp',components: { TodoHeader, TodoFooter, TodoItem },data () {return {todos: []}},methods: {handleNewTodo (text) {const lastTodo = this.todos[this.todos.length - 1]this.todos.push({id: lastTodo ? lastTodo.id + 1 : 1,text,done: false})}}
}
</script>

TodoItem 组件

内容展示&处理完成状态

编写测试用例

// src\components\TodoApp\__tests__\TodoItem.js
import { shallowMount } from '@vue/test-utils'
import TodoItem from '@/components/TodoApp/TodoItem'describe('TodoItem.vue', () => {// 使用 vscode 注解声明 type 以使用类型提示/** @type {import('@vue/test-utils').Wrapper} */let wrapper = nullbeforeEach(() => {const todo = {id: 1,text: 'play',done: true}wrapper = shallowMount(TodoItem, {propsData: {todo}})})test('text', () => {// 断言文本内容expect(wrapper.findComponent('[data-testid="todo-text"]').text()).toBe(wrapper.vm.todo.text)})test('done', async () => {const done = wrapper.findComponent('[data-testid="todo-done"]')const todoItem = wrapper.findComponent('[data-testid="todo-item"]')// 断言完成状态expect(done.element.checked).toBeTruthy()// 断言 class - classes(获取 DOM 节点的 class 数组)expect(todoItem.classes()).toContain('completed')// 修改复选框状态,并等待视图更新await done.setChecked(false)// 断言 classexpect(todoItem.classes('completed')).toBeFalsy()})
})

TodoApp 组件中向子组件 TodoItem 传入必填 prop,否则测试会报错:

<TodoItem v-for="todo in todos" :key="todo.id" :todo="todo" />

完善组件功能

<!-- src\components\TodoApp\TodoItem.vue -->
<template><li data-testid="todo-item" :class="{completed:todo.done}"><div class="view"><!-- eslint-disable-next-line vue/no-mutating-props --><input v-model="todo.done" data-testid="todo-done" class="toggle" type="checkbox" /><label data-testid="todo-text">{{ todo.text }}</label><button class="destroy"></button></div><input class="edit" value="Create a TodoMVC template" /></li>
</template><script>
export default {name: 'TodoItem',props: {todo: {type: Object,required: true}}
}
</script>

删除任务

TodoItem 点击删除按钮,向父组件发送删除事件,并把任务项的 id 传递给父组件,父组件通过一个方法处理删除操作。

编写测试用例

test('delete todo', async () => {const deleteBtn = wrapper.findComponent('[data-testid="delete"]')await deleteBtn.trigger('click')expect(wrapper.emitted()['delete-todo']).toBeTruthy()expect(wrapper.emitted()['delete-todo'][0][0]).toBe(wrapper.vm.todo.id)
})

完善组件功能

<button data-testid="delete" class="destroy" @click="$emit('delete-todo', todo.id)"></button>

TodoApp 删除操作

编写测试用例

// src\components\TodoApp\__tests__\TodoApp.js
import { shallowMount } from '@vue/test-utils'
import TodoApp from '@/components/TodoApp'
import TodoItem from '@/components/TodoApp/TodoItem'describe('TodoApp.vue', () => {/** @type {import('@vue/test-utils')/Wrapper} */let wrapper = nullbeforeEach(async () => {wrapper = shallowMount(TodoApp)const todos = [{ id: 1, text: 'eat', done: false },{ id: 2, text: 'play', done: true },{ id: 3, text: 'sleep', done: false }]// 初始化默认数据,并等待视图更新await wrapper.setData({todos})})test('New todo', async () => {const text = 'play'// 调用组件的方法,添加任务项wrapper.vm.handleNewTodo(text)// 期望管理的数组中包含刚添加的任务项const todo = wrapper.vm.todos.find(t => t.text === text)expect(todo).toBeTruthy()})test('Todo List', async () => {// 期望指定子组件被渲染了3个expect(wrapper.findAllComponents(TodoItem).length).toBe(wrapper.vm.todos.length)})test('Delete Todo', async () => {// 正向测试 传递一个真实的 idawait wrapper.vm.handleDeleteTodo(1)expect(wrapper.vm.todos.length).toBe(2)expect(wrapper.findAllComponents(TodoItem).length).toBe(2)})test('Delete Todo', async () => {// 反向测试 传递要给不存在的 idawait wrapper.vm.handleDeleteTodo(123)expect(wrapper.vm.todos.length).toBe(3)expect(wrapper.findAllComponents(TodoItem).length).toBe(3)})
})

完善组件功能

<!-- src\components\TodoApp\index.vue -->
<template><section class="todoapp"><TodoHeader @new-todo="handleNewTodo" /><section class="main"><input id="toggle-all" class="toggle-all" type="checkbox" /><label for="toggle-all">Mark all as complete</label><ul class="todo-list"><!-- These are here just to show the structure of the list items --><!-- List items should get the class `editing` when editing and `completed` when marked as completed --><TodoItemv-for="todo in todos":key="todo.id":todo="todo"@delete-todo="handleDeleteTodo"/></ul></section><!-- This footer should be hidden by default and shown when there are todos --><TodoFooter /></section>
</template><script>
import TodoHeader from './TodoHeader'
import TodoFooter from './TodoFooter'
import TodoItem from './TodoItem'export default {name: 'TodoApp',components: { TodoHeader, TodoFooter, TodoItem },data () {return {todos: []}},methods: {handleNewTodo (text) {const lastTodo = this.todos[this.todos.length - 1]this.todos.push({id: lastTodo ? lastTodo.id + 1 : 1,text,done: false})},handleDeleteTodo (todoId) {const index = this.todos.findIndex(t => t.id === todoId)if (index !== -1) {this.todos.splice(index, 1)}}}
}
</script>

TodoItem 双击获得编辑状态

编写测试用例

test('edit todo style', async () => {const label = wrapper.findComponent('[data-testid="todo-text"]')const todoItem = wrapper.findComponent('[data-testid="todo-item"]')const todoEdit = wrapper.findComponent('[data-testid="todo-edit"]')// 触发双击事件await label.trigger('dblclick')// 断言 classexpect(todoItem.classes()).toContain('editing')// 失去焦点await todoEdit.trigger('blur')expect(todoItem.classes('editing')).toBeFalsy()
})

完善组件功能

<!-- src\components\TodoApp\TodoItem.vue -->
<template><li data-testid="todo-item" :class="{completed: todo.done,editing: isEditing}"><div class="view"><!-- eslint-disable-next-line vue/no-mutating-props --><input v-model="todo.done" data-testid="todo-done" class="toggle" type="checkbox" /><label data-testid="todo-text" @dblclick="isEditing=true">{{ todo.text }}</label><button data-testid="delete" class="destroy" @click="$emit('delete-todo', todo.id)"></button></div><inputclass="edit"value="Create a TodoMVC template"data-testid="todo-edit"@blur="isEditing=false"/></li>
</template><script>
export default {name: 'TodoItem',props: {todo: {type: Object,required: true}},data () {return {isEditing: false}}
}
</script>

TodoItem 自动获得焦点

通过自定义指令实现自动获得焦点。

<!-- src\components\TodoApp\TodoItem.vue -->
<template><li data-testid="todo-item" :class="{completed: todo.done,editing: isEditing}"><div class="view"><!-- eslint-disable-next-line vue/no-mutating-props --><input v-model="todo.done" data-testid="todo-done" class="toggle" type="checkbox" /><label data-testid="todo-text" @dblclick="isEditing=true">{{ todo.text }}</label><button data-testid="delete" class="destroy" @click="$emit('delete-todo', todo.id)"></button></div><inputv-focus="isEditing"class="edit"value="Create a TodoMVC template"data-testid="todo-edit"@blur="isEditing=false"/></li>
</template><script>
export default {name: 'TodoItem',props: {todo: {type: Object,required: true}},directives: {focus (element, binding) {if (binding.value) {element.focus()}}},data () {return {isEditing: false}}
}
</script>

TodoItem 保存修改

编写测试用例

test('save edit todo', async () => {const label = wrapper.findComponent('[data-testid="todo-text"]')const todoEdit = wrapper.findComponent('[data-testid="todo-edit"]')// 触发双击事件await label.trigger('dblclick')// 编辑文本框中的内容展示expect(todoEdit.element.value).toBe(wrapper.vm.todo.text)// 修改文本框的值const text = 'Hello'await todoEdit.setValue(text)// 触发回车保存事件await todoEdit.trigger('keyup.enter')// 断言是否对外发送一个自定义事件expect(wrapper.emitted()['edit-todo']).toBeTruthy()expect(wrapper.emitted()['edit-todo'][0][0]).toEqual({id: wrapper.vm.todo.id,text})// 断言编辑状态被取消expect(wrapper.vm.isEditing).toBeFalsy()
})

完善组件功能

// template
<inputv-focus="isEditing"class="edit":value="todo.text"data-testid="todo-edit"@blur="isEditing=false"@keyup.enter="handleEditTodo"/>// js
methods: {handleEditTodo (e) {this.$emit('edit-todo', {id: this.todo.id,text: e.target.value})// 取消编辑状态this.isEditing = false}
}

TodoApp 保存修改

编写测试用例

test('Edit Todo', async () => {const todo = { id: 2, text: 'abc' }// 修改任务await wrapper.vm.handleEditTodo(todo)expect(wrapper.vm.todos[1].text).toBe(todo.text)// 内容为空时删除任务todo.text = ''await wrapper.vm.handleEditTodo(todo)expect(wrapper.vm.todos.find(t => t.id === todo.id)).toBeFalsy()
})

完善组件功能

// template
<TodoItemv-for="todo in todos":key="todo.id":todo="todo"@delete-todo="handleDeleteTodo"@edit-todo="handleEditTodo"
/>// js
handleEditTodo ({ id, text }) {const todo = this.todos.find(t => t.id === id)if (!todo) {return}if (!text.trim().length) {// 执行删除操作return this.handleDeleteTodo(id)}// 执行修改操作todo.text = text
}

TodoItem 取消编辑

编写测试用例

test('cancel edit todo', async () => {const label = wrapper.findComponent('[data-testid="todo-text"]')const todoEdit = wrapper.findComponent('[data-testid="todo-edit"]')// 触发双击事件await label.trigger('dblclick')// 备份原内容const text = wrapper.vm.todo.text// 修改内容await todoEdit.setValue('bbb')// 触发 ESC 取消事件await todoEdit.trigger('keyup.esc')// 断言内容没有被修改expect(wrapper.vm.todo.text).toBe(text)// 断言编辑状态被取消expect(wrapper.vm.isEditing).toBeFalsy()
})

完善组件功能

// template
<inputv-focus="isEditing"class="edit":value="todo.text"data-testid="todo-edit"@blur="isEditing=false"@keyup.enter="handleEditTodo"@keyup.esc="handleCancelEdit"/>// js
handleCancelEdit () {this.isEditing = false
}

Vue2 应用测试学习 02 - TDD 案例(案例介绍和创建、编写测试用例)相关推荐

  1. 初入测试如何编写测试用例?从3个方面带你写一个合格的测试用例

    前言 作为一个测试新人,刚开始接触测试,对于怎么写测试用例很头疼,无法接触需求,只能根据站在用户的角度去做测试,但是这样情况会导致不能全方位的测试APP,这种情况就需要一份测试用例了,但是不会写,求指 ...

  2. 渗透测试学习笔记之案例二

    0x00 前言 渗透是个持续的过程,不断地搜集信息,整理信息,以及利用信息,最终的目标就是拿到系统乃至整个网络的最高权限.在笔者看来,渗透测试与安全研究的最大不同就是前者擅长利用后者的研究成果并运用到 ...

  3. E2E 端到端测试学习 - E2E 介绍、Cypress 案例基本使用

    E2E 测试介绍 E2E E2E(end to end)端到端测试是最直观可以理解的测试类型.在前端应用程序中,端到端测试可以从用户的视角通过真实浏览器自动检查应用程序是否正常工作. E2E 把整个系 ...

  4. MyBatis-学习笔记02【02.Mybatis入门案例】

    Java后端 学习路线 笔记汇总表[黑马程序员] MyBatis-学习笔记01[01.Mybatis课程介绍及环境搭建][day01] MyBatis-学习笔记02[02.Mybatis入门案例] M ...

  5. vue2中vant实现网易云音乐案例-附带所有源码

    vue2中vant实现网易云音乐案例-附带所有源码 前言 学习笔记以及源码下载gitee: https://gitee.com/xingyueqianduan/vantmsicdemo 下载下来的内容 ...

  6. Cookie和Session-学习笔记02【Cookie案例、JSP改造Cookie案例】

    Java后端 学习路线 笔记汇总表[黑马程序员] Cookie和Session-学习笔记01[Cookie_快速入门.Cookie_细节] Cookie和Session-学习笔记02[Cookie案例 ...

  7. ExtJS学习:MVC模式案例(三)

    在ExtJS案例系列教程的前两讲,我们了解了该案例的最终实现效果,并且在ExtJS学习:MVC模式案例(二)中我们添加Viewport.js和demoController.js两个文件,实现了对整个网 ...

  8. 爬虫学习:基础爬虫案例实战

    爬虫学习:基础爬虫案例实战 文章目录 爬虫学习:基础爬虫案例实战 一.前言 二.案例实战 任务一:爬取列表页 任务二:爬取详细页 任务三:保存爬取数据 任务四:利用多进程提高效率 三.补充一点 四.最 ...

  9. PyTorch机器学习与深度学习技术方法与案例实践应用

    目录 第一章.Python基础知识串讲 第二章.PyTorch简介与环境搭建 第三章.PyTorch编程入门与进阶 第四章.PyTorch前向型神经网络 第五章.PyTorch卷积神经网络 第六章.P ...

最新文章

  1. 关于学习Python的一点学习总结(50->描述符)
  2. MYSQL连接字符串参数详细解析(大全参考)
  3. 要出去找工作了,看看多线程和并行的区别及什么时候该用
  4. Visual c++6.0 如何自定义一个光标,使其变成字
  5. shell 工具_shell 脚本加密工具 - shc
  6. 决战9小时,产品上线的危机时刻
  7. java 面试心得总结-BAT、网易
  8. html位置下移像素点,吃透移动端 1px的具体用法
  9. 前端学习(2876):原生js模块化+弹幕的宽度和高度
  10. Keras-7 Reuters, a multiclass classification example
  11. Vagrant+PHPStorm+Google+XDebug断点调试
  12. linux+查看系统环境路径,Linux环境变量设置方法总结 PATH、LD_LIBRARY_PATH
  13. leetcode之奇偶链表
  14. 卡方分布的定义与概率密度的计算
  15. 修复40G的老IDE硬盘
  16. HTML、CSS制作小米商城网页首页源码解析
  17. 【企业开源】小米开源:站在巨人肩膀上的创新
  18. 科学计算机可以用多久,科学家公布“寿命计算器” 算一下你能活多久?
  19. Python编程基础题(2-求一元二次方程的解Ⅱ)
  20. PowerShell脚本免杀/bypass/绕过杀毒软件

热门文章

  1. 三星手机的计算机功能在哪里,三星手机已连接到计算机,详细教您如何将三星手机连接到计算机...
  2. css获取vue组件 props
  3. 华为花瓣搜索的新解读:让开发者透过垂直生态,掘金全球
  4. 记录一下新写的软件框架(Clover)
  5. Angular7设置Input的disabled属性
  6. 【Java工具类】HutoolUtil
  7. python画成绩正态分布图_终于搞清楚正态分布、指数分布到底是啥了!
  8. Java编程 Javase06
  9. struts标签html:select,JSP_Struts标签 htmlselect option基本用法.doc
  10. 小米信息部面试软件测试出过的题