Jest学习笔记(4)-- 以 TodoMVC 为例编写测试文件

107 阅读4分钟

TodoHeader组件

<template>
  <header class="header">
    <h1>todos</h1>
    <input
      class="new-todo"
      placeholder="What needs to be done?"
      autofocus
      data-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>

<style lang="scss" scoped>

</style>

  • 创建测试文件 就近原则
  • 创建__test__文件\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);
    const input = wrapper.find('input[data-testid="new-todo"]');
    const text = 'zhangweihai';
    // 模拟输入
    await input.setValue(text);
    // 模拟点击
    await input.trigger('keyup.enter');
    // 事件被触发
    expect(wrapper.emitted()['new-todo']).toBeTruthy();
    // 触发emit后 传出值为input输入值
    expect(wrapper.emitted()['new-todo'][0][0]).toBe(text);
    // 事件触发完成后输入框清空
    expect(input.element.value).toBe('');
   })
})
  • package.json中配置"test": "jest"

image.png

  • 执行单个文件
npm run test src/components/TodoApp/__tests__/TodoHeader.js
  • 上述测试文件中 模拟了用户输入,触发事件,清空输入框

image.png

  • 可以通过vue工具查看验证

image.png

TodoApp组件

<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">
        <TodoItem v-for="todo in todos" :key="todo.id" :todo="todo" />
      </ul>
    </section>
    <TodoFooter />
  </section>
</template>

<script>
import TodoHeader from './TodoHeader.vue';
import TodoFooter from './TodoFooter.vue';
import TodoItem from './TodoItem.vue';
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>

// TodoApp.js
import { shallowMount } from "@vue/test-utils";
import TodoApp from '@/components/TodoApp/index.vue';
import TodoItem from '@/components/TodoApp/TodoItem.vue'
describe('TodoApp', () => {
  test('new todo', () => {
    const wrapper = shallowMount(TodoApp);
    const text = 'play';
    // 执行handleNewTodo往todos中添加数据
    wrapper.vm.handleNewTodo(text);
    const todo = wrapper.vm.todos.find(item => item.text === text);
    expect(todo.text).toBeTruthy();
  })

  // 测试列表展示
  test('todo list', async () => {
    const wrapper = shallowMount(TodoApp);

    // 写法1
    const todos = [
      {id: 1, text: 'song', done: false},
      {id: 2, text: 'play', done: true},
      {id: 3, text: 'basketball', done: false},
    ]
    await wrapper.setData({
      todos
    })
    expect(wrapper.findAllComponents(TodoItem).length).toBe(todos.length);
    // 写法2
    /* wrapper.vm.todos = [
      {id: 1, text: 'song', done: false},
      {id: 2, text: 'play', done: true},
      {id: 3, text: 'basketball', done: false},
    ]
    await Vue.nextTick() */
  })
})

TodoItem组件

<TodoItem v-for="todo in todos" :key="todo.id" :todo="todo" />
<template>
  <!-- List items should get the class `editing` when editing and `completed` when marked as completed -->
  <!-- 绑定class -->
  <li data-testid="todo-item" :class="{completed: todo.done}">
    <div class="view">
      <!-- 绑定v-model -->
      <input v-model="todo.done" data-testid="todo-done" class="toggle" type="checkbox" />
      <!-- 绑定text -->
      <label data-testid="todo-text">{{ todo.text }}</label>
      <button class="destroy"></button>
    </div>
    <input class="edit" value="Rule the web" />
  </li>
</template>

<script>
export default {
  name: 'TodoItem',
  props: {
    todo: {
      type: Object,
      required: true
    }
  }
}
</script>

<style lang="scss" scoped>

</style>

// TodoItem.js
import { shallowMount } from "@vue/test-utils";
import TodoItem from '@/components/TodoApp/TodoItem.vue'
describe('TodoItem', () => {
  test('todo-item show', () => {
    const todo = {
      id: 1,
      text: 'play',
      done: true,
    }
    const wrapper = shallowMount(TodoItem, {
      propsData: {
        todo
      }
    });
    // 文本内容
    expect(wrapper.find('[data-testid="todo-text"]').text()).toBe(todo.text);
    // 选中状态
    expect(wrapper.find('[data-testid="todo-done"]').element.checked).toBeTruthy();
    // 选中新增class类名
    expect(wrapper.find('[data-testid="todo-item"]').classes()).toContain('completed');
    
    // 未选中状态
    // expect(wrapper.find('[data-testid="todo-done"]').element.checked).toBeFalsy();
    // expect(wrapper.find('[data-testid="todo-item"]').classes().length).toBe(0);
  })
})

  • 优化写法 测试分模块
import { shallowMount } from "@vue/test-utils";
import TodoItem from '@/components/TodoApp/TodoItem.vue'
describe('TodoItem.vue', () => {
  // 优化
  /** @type {import('@vue/test-utils').Wrapper} */
  let wrapper = null;
  beforeEach(() => {
    const todo = {
      id: 1,
      text: 'play',
      done: true,
    }
    wrapper = shallowMount(TodoItem, {
      propsData: {
        todo
      }
    });
  })
  test('text', () => {
    // 文本内容
    expect(wrapper.find('[data-testid="todo-text"]').text()).toBe(wrapper.vm.todo.text);
  });

  test('done', async () => {
    const done = wrapper.find('[data-testid="todo-done"]');
    const todoItem = wrapper.find('[data-testid="todo-item"]');
    // 选中状态
    expect(done.element.checked).toBeTruthy();
    // 选中新增class类名
    expect(todoItem.classes()).toContain('completed');

    // 更改选中状态
    await done.setChecked(false);
    expect(todoItem.classes('completed')).toBeFalsy();
  })
})

  • 有个细节问题 初始将wrapper置为null后失去了智能提示
  • 解决方案
/** @type {import('@vue/test-utils').Wrapper} */
let wrapper = null;

image.png

  • TodoItem.vue新增删除逻辑
<button data-testid="todo-del" class="destroy" @click="handleDelete(todo.id)"></button>
<script>
export default {
  name: 'TodoItem',
  props: {
    todo: {
      type: Object,
      required: true
    }
  },
  methods: {
    handleDelete (id) {
      this.$emit('delete-todo', id)
    }
  }
}
</script>
  • TodoItem.js验证
test('delete', async () => {
    const delBtn = wrapper.find('button[data-testid="todo-del"]');
    await delBtn.trigger('click');
    expect(wrapper.emitted()['delete-todo']).toBeTruthy();
    expect(wrapper.emitted()['delete-todo'][0][0]).toBe(wrapper.vm.todo.id);
})
  • 同理 验证父组件删除逻辑
<TodoItem v-for="todo in todos" :key="todo.id" :todo="todo"
  @delete-todo="handleDeleteTodo"
/>
export default {
  methods: {
    handleDeleteTodo (todoId) {
      const index = this.todos.findIndex(item => item.id == todoId);
      if (index != -1) {
        this.todos.splice(index, 1);
      }
    }
  }
}
// TodoApp.js
import { shallowMount } from "@vue/test-utils";
import TodoApp from '@/components/TodoApp/index.vue';
import TodoItem from '@/components/TodoApp/TodoItem.vue'
describe('TodoApp', () => {
  /** @type {import('@vue/test-utils').Wrapper} */
  let wrapper = null;
  beforeEach(async () => {
    wrapper = shallowMount(TodoApp);
    const todos = [
      {id: 1, text: 'song', done: false},
      {id: 2, text: 'play', done: true},
      {id: 3, text: 'basketball', done: false},
    ]
    await wrapper.setData({
      todos
    })
  })
  test('new todo', () => {
    const text = 'play';
    // 执行handleNewTodo往todos中添加数据
    wrapper.vm.handleNewTodo(text);
    // 此时todos中新增了一条text数据
    const todo = wrapper.vm.todos.find(item => item.text === text);
    expect(todo.text).toBeTruthy();
  })

  // 测试列表展示
  test('todo list', async () => {
    expect(wrapper.findAllComponents(TodoItem).length).toBe(wrapper.vm.todos.length);
  })

  test('测试子传父删除功能', async () => {
    // 执行handleDeleteTodo
    await wrapper.vm.handleDeleteTodo(1);
    expect(wrapper.vm.todos.length).toBe(2);
    // dom更新是异步的 所以加上await
    expect(wrapper.findAllComponents(TodoItem).length).toBe(2);
  })

  test('测试子传父删除功能--反向测试', async () => {
    // 执行handleDeleteTodo
    await wrapper.vm.handleDeleteTodo(111);
    expect(wrapper.vm.todos.length).toBe(3);
    // dom更新是异步的 所以加上await
    expect(wrapper.findAllComponents(TodoItem).length).toBe(3);
  })
})

image.png

  • TodoItem 双击获得编辑状态
<template>
  <!-- List items should get the class `editing` when editing and `completed` when marked as completed -->
  <!-- 绑定class -->
  <li
    data-testid="todo-item"
    :class="{
      completed: todo.done,
      editing: isEditing === true
    }"
  >
    <div class="view">
      <!-- 绑定v-model -->
      <input v-model="todo.done" data-testid="todo-done" class="toggle" type="checkbox" />
      <!-- 绑定text -->
      <label
        data-testid="todo-text"
        @dblclick="isEditing = true"
      >{{ todo.text }}</label>
      <button data-testid="todo-del" class="destroy" @click="handleDelete(todo.id)"></button>
    </div>
    <input
      v-focus="isEditing"
      data-testid="todo-edit"
      class="edit"
      :value="todo.text"
      @blur="isEditing = false"
    />
  </li>
</template>

<script>
export default {
  name: 'TodoItem',
  props: {
    todo: {
      type: Object,
      required: true
    }
  },
  directives: {
    // 自动获取焦点
    focus (el, binding) {
      if (binding.value) {
        el.focus();
      }
    }
  },
  data () {
    return {
      isEditing: false
    }
  },
  methods: {
    handleDelete (id) {
      this.$emit('delete-todo', id)
    }
  }
}
</script>

<style lang="scss" scoped>

</style>

  • TodoItem.js
test('双击编辑', async () => {
    const todoItem = wrapper.find('li[data-testid="todo-item"]');
    const label = wrapper.find('label[data-testid="todo-text"]');
    const todoEdit = wrapper.find('input[data-testid="todo-edit"]');
    // 双击
    await label.trigger('dblclick');
    // 新增类名editing
    expect(todoItem.classes()).toContain('editing');
    // input失去焦点
    await todoEdit.trigger('blur');
    // 删除类名
    expect(todoItem.classes('editing')).toBeFalsy();
})
  • TodoItem 回车保存 esc取消编辑
<input
  v-focus="isEditing"
  data-testid="todo-edit"
  class="edit"
  :value="todo.text"
  @blur="isEditing = false"
  @keyup.enter="handleEdit"
/>
<script>
export default {
  name: 'TodoItem',
  methods: {
    handleEdit (e) {
      this.$emit('edit-todo', {
        id: this.todo.id,
        text: e.target.value
      });
      // 取消编辑状态
      this.isEditing = false;
    }
  }
}
</script>
test('保存编辑', async () => {
    const label = wrapper.find('label[data-testid="todo-text"]');
    const todoEdit = wrapper.find('input[data-testid="todo-edit"]');
    // 双击
    await label.trigger('dblclick');
    // 编辑文本框中的内容展示
    expect(todoEdit.element.value).toBe(wrapper.vm.todo.text);
    // 修改文本框的值
    const text = 'hello';
    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();
});
  • 子组件中触发emit,所以我们需要完善父组件以及父组件测试文件
<TodoItem v-for="todo in todos" :key="todo.id" :todo="todo"
      @delete-todo="handleDeleteTodo"
      @edit-todo="handleEditTodo"
/>
<script>
export default {
  name: 'TodoApp',
  methods: {
    handleEditTodo ({id, text}) {
      const todo = this.todos.find(item => item.id == id);
      if (!todo) {
        return;
      }
      if (!text.trim().length) {
        // 执行删除操作
        this.handleDeleteTodo(id);
        return;
      }
      // 执行修改
      todo.text = text;
    }
  }
}
</script>
// TodoApp.js
test('测试子传父编辑功能', async () => {
    const todo = {id: 2, text: 'jump'};
    // 执行 handleEditTodo
    await wrapper.vm.handleEditTodo(todo);
    // todos数组第一项的text改为jump
    expect(wrapper.vm.todos[1].text).toBe(todo.text);

    // 反向测试 当todo.text清空时 删除当前项
    todo.text = '';
    await wrapper.vm.handleEditTodo(todo);
    expect(wrapper.vm.todos.find(item => item.id === todo.id)).toBeFalsy();
})
  • TodoItem取消编辑
// TodoItem.vue
<input
      v-focus="isEditing"
      data-testid="todo-edit"
      class="edit"
      :value="todo.text"
      @blur="isEditing = false"
      @keyup.enter="handleEdit"
      @keyup.esc="handleCancelEdit"
/>
handleCancelEdit () {
  this.isEditing = false;
}
// TodoItem.js
test('取消编辑', async () => {
    const label = wrapper.find('label[data-testid="todo-text"]');
    const todoEdit = wrapper.find('input[data-testid="todo-edit"]');
    // 双击
    await label.trigger('dblclick');
    // text备份
    const text = wrapper.vm.todo.text;
    // 设置值
    todoEdit.setValue('随便任何输入内容');
    // 触发取消
    await todoEdit.trigger('keyup.esc');
    // 验证字段没有被修改
    expect(wrapper.vm.todo.text).toBe(text);
    // 验证编辑状态被取消
    expect(wrapper.vm.isEditing).toBeFalsy();
  });
  • 设置全选
// TodoApp/index.vue
<input
    data-testid="toggle-all"
    id="toggle-all"
    class="toggle-all"
    type="checkbox"
    v-model="toggleAll"
/>
<script>
    computed: {
        toggleAll: {
          get () {
            // 是否全选
            return this.todos.length && this.todos.every(item => item.done);
          },
          set (value) {
            this.todos.forEach(item => {
              item.done = value;
            })
          }
        }
    },
</script>
// TodoApp.js
test('测试全选', async () => {
    const toggleAll = wrapper.find('input[data-testid="toggle-all"]');
    // 设置全选
    toggleAll.setChecked();
    // 断言所有的子任务都被选中
    wrapper.vm.todos.forEach(item => {
      expect(item.done).toBeTruthy();
    })

    // 取消全选
    toggleAll.setChecked(false);
    wrapper.vm.todos.forEach(item => {
      expect(item.done).toBeFalsy();
    })
  });

  test('测试全选与子项联动', async () => {
    const toggleAll = wrapper.find('input[data-testid="toggle-all"]');
    // 让所有任务都变成选中状态
    wrapper.vm.todos.forEach(item => {
      item.done = true;
    })
    await Vue.nextTick();
    // 断言toggleAll也被选中
    expect(toggleAll.element.checked).toBeTruthy();
    // 取消某个任务
    wrapper.vm.todos[0].done = false;
    // 断言toggleAll未被选中
    await Vue.nextTick();
    expect(toggleAll.element.checked).toBe(false);
    
    // todos设置为空
    wrapper.vm.todos = [];
    // 断言toggleAll未被选中
    await Vue.nextTick();
    expect(toggleAll.element.checked).toBe(false);
  });

TodoFooter.vue

  • 父组件
<TodoFooter :todos="todos" />
// TodoFooter.vue
<template>
  <footer class="footer">
    <!-- This should be `0 items left` by default -->
    <span class="todo-count"><strong data-testid="done-todos-count">{{ doneTodosCount }}</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',
  props: {
    todos: {
      type: Object,
      required: true
    }
  },
  computed: {
    doneTodosCount () {
      return this.todos.lenght && this.todos.filter(todo => !todo.done).lenght;
    }
  }
}
</script>

<style lang="scss" scoped>

</style>
// TodoFooter.js
import { shallowMount } from "@vue/test-utils";
import TodoFooter from '@/components/TodoApp/TodoFooter.vue'

describe('TodoFooter', () => {
  /** @type {import('@vue/test-utils').Wrapper} */
  let wrapper = null;
  beforeEach(() => {
    const todos = [
      {id: 1, text: 'song', done: false},
      {id: 2, text: 'play', done: true},
      {id: 3, text: 'basketball', done: false},
    ]
    wrapper = shallowMount(TodoFooter, {
      propsData: {
        todos
      }
    });
  });

  test('测试剩余任务数量是否正确', () => {
    // 未完成数量
    const count = wrapper.vm.todos.filter(item => !item.done).length;
    const countEl = wrapper.find('strong[data-testid="done-todos-count"]');
    // 断言文本显示数字等于count
    expect(Number.parseInt(countEl.text())).toBe(count);
  })
})

  • TodoFooter清除已完成任务的显示状态
<button
  v-if="isClearCompletedShown"
  data-testid="clear-completed"
  class="clear-completed">
    Clear completed
</button>
<script>
computed: {
    isClearCompletedShown () {
      return this.todos.length && this.todos.find(todo => todo.done);
    }
}
</script>
// TodoFooter.js
test('清除按钮是否展示', async () => {
    const clearBtn = wrapper.find('button[data-testid="clear-completed"]');
    // 因为初始数据有已完成状态 则clearBtn显示 有done为true 则断言按钮是已渲染状态
    expect(clearBtn.exists()).toBeTruthy();

    // 清除所有任务的完成状态 断言clearBtn不存在
    // 该写法有问题 不能直接修改propsData
    /* wrapper.vm.todos.forEach(item => {
      item.done = false;
    });
    await Vue.nextTick(); */
    wrapper = shallowMount(TodoFooter, {
      propsData: {
        todos: [
          {id: 1, text: 'song', done: false},
          {id: 2, text: 'play', done: false},
          {id: 3, text: 'basketball', done: false},
        ]
      }
    });
    // 隐藏按钮
    expect(wrapper.find('button[data-testid="clear-completed"]').exists()).toBeFalsy();
  });
  • 删除已完成任务
<TodoFooter :todos="todos"
  @clear-completed="handleClearCompleted"
/>
<script>
 methods: {
  handleClearCompleted () {
      // 清除所有已完成任务
      for (let i = 0; i < this.todos.length; i++) {
        if (this.todos[i].done) {
          // delete
          this.todos.splice(i, 1);
          i--;
        }
      }
    }   
 }
</script>
// TodoApp.js
test('清除已完成任务项', async () => {
    wrapper.vm.handleClearCompleted();
    await Vue.nextTick();
    expect(wrapper.vm.todos).toEqual([
      {id: 1, text: 'song', done: false},
      {id: 3, text: 'basketball', done: false},
    ]);
});
  • TodoApp筛选数据
  • 实现思路
    • 将路由导航到 / 断言 filterTodos = 所有的任务
    • 将路由导航到 /active 断言 filterTodos = 所有的未完成任务
    • 将路由导航到 /completed 断言 filterTodos = 所有的已完成任务
  • 参考Vue Test Utils关于伪造 $route 和 $router的介绍
// TodoApp.js
beforeEach(async () => {
    const $route = {
      path: '/'
    }
    wrapper = shallowMount(TodoApp, {
      mocks: {
        $route
      }
    });
});
  • 对项目进行改造,使用路由
// router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import TodoApp from '@/components/TodoApp/index.vue'
Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    component: TodoApp
  },
  {
    path: '/active',
    component: TodoApp
  },
  {
    path: '/completed',
    component: TodoApp
  },
]

const router = new VueRouter({
  routes
})

export default router

// index.vue
<TodoItem
  v-for="todo in filterTodos"
  :key="todo.id"
  :todo="todo"
  @delete-todo="handleDeleteTodo"
  @edit-todo="handleEditTodo"
/>
<script>
    computed: {
        // 过滤数据
        filterTodos () {
          // 获取路由路径
          // 根据路由路径过滤数据
          const path = this.$route.path;
          switch (path) {
            //  / 所有的任务列表
            case '/':
              return this.todos;
            // /active 所有的未完成任务
            case '/active':
              return this.todos.filter((todo) => !todo.done);
            // /completed 所有的已完成任务
            case '/completed':
              return this.todos.filter((todo) => todo.done);
            default:
              return this.todos;
          }
        },
    }
</script>
// TodoApp.js
test('过滤数据', async () => {
    // 将路由导航到 /
    wrapper.vm.$route.path = '/';
    await Vue.nextTick();
    // 断言 filterTodos = 所有的任务
    expect(wrapper.vm.filterTodos).toEqual([
      {id: 1, text: 'song', done: false},
      {id: 2, text: 'play', done: true},
      {id: 3, text: 'basketball', done: false},
    ]);

    // 将路由导航到 /active
    wrapper.vm.$route.path = '/active';
    await Vue.nextTick();
    // 断言 filterTodos = 所有的未完成任务
    expect(wrapper.vm.filterTodos).toEqual([
      {id: 1, text: 'song', done: false},
      {id: 3, text: 'basketball', done: false},
    ]);

    // 将路由导航到 /completed
    wrapper.vm.$route.path = '/completed';
    await Vue.nextTick();
    // 断言 filterTodos = 所有的已完成任务
    expect(wrapper.vm.filterTodos).toEqual([
      {id: 2, text: 'play', done: true},
    ]);
  });
  • TodoFooter导航链接高亮
// TodoFooter.vue
<ul class="filters">
  <li>
    <!-- <a class="selected" href="#/">All</a> -->
    <router-link exact to="/">All</router-link>
  </li>
  <li>
    <!-- <a href="#/active">Active</a> -->
    <router-link to="/active">Active</router-link>
  </li>
  <li>
    <!-- <a href="#/completed">Completed</a> -->
    <router-link to="/completed">Completed</router-link>
  </li>
</ul>
  • TodoFooter.js
import { createLocalVue, mount } from "@vue/test-utils";
import TodoFooter from '@/components/TodoApp/TodoFooter.vue';
import VueRouter from 'vue-router';
// 创建局部Vue
const localVue = createLocalVue();
localVue.use(VueRouter);
const router = new VueRouter({
  linkActiveClass: 'selected'
});
beforeEach(() => {
    // 涉及到 样式断言 linkActiveClass: 'selected' 需要深渲染
    wrapper = mount(TodoFooter, {
      propsData: {
        todos
      },
      localVue,
      router
    });
});

test('测试导航链接的激活状态', async () => {
    // 找到所有导航链接
    const links = wrapper.findAllComponents({
      name: 'RouterLink'
    });
    router.push('/active');
    await localVue.nextTick();
    for (let i = 0; i < links.length; i++) {
      // 被wrapper包装过后的组件
      const link = links.at(i);
      if (link.vm.to === '/active') {
        expect(link.classes()).toEqual(["router-link-exact-active", "selected"]);
      } else {
        expect(link.classes('seleted')).toBeFalsy();
      }
    }
  });

快照测试

  • 当项目比较稳定的时候,加上快照测试,及时检测无意间的修改,真的想修改也得二次确认
// TodoHeader.js
test('快照测试-TodoHeader', () => {
    expect(wrapper.html()).toMatchSnapshot();
});
  • 生成对应的快照

image.png

  • 生成测试覆盖率统计报告
// package.json
"scripts": {
    "coverage": "vue-cli-service test:unit --coverage",
},

image.png

image.png