Vue2アプリケーションテスト学習02 - TDDケース(ケースの紹介と作成、テストケースの書き方)

事例紹介

TodoMVC が提供するオープンソースの Todos List タスク リストの例を使用して、 TDDを使用して Vue アプリケーションを開発する方法を学びましょう

事例デモ効果:Vue.js・TodoMVC

基本的な機能は次のとおりです。

  • タスクの内容を入力し、「Enter」をクリックすると、タスクがリストに追加されます。
  • 単一またはすべてのタスクの完了ステータスを切り替えます
  • タスクを削除する
  • 未完了のタスクの数を表示する
  • 完了したタスクをすべて削除する
  • データフィルタリング
  • タスクの内容を変更します(変更を保存するには Enter を押し、編集をキャンセルするには ESC を押します。内容が空の場合はタスクを削除します)。

ケース作成

コンポーネント ファイルを作成し、 src\components\TodoApp\index.vueGithub リポジトリの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 = false

new Vue({
    
    
  router,
  store,
  render: h => h(App)
}).$mount('#app')

npm run serveアプリケーションを起動して効果を確認してくださいhttp://localhost:8080/

コンポーネントユニットを分割する

次に、TDD(テスト駆動開発)手法に従ってアプリケーションを開発し、まず機能をコンポーネントに分割します。

Todoヘッダー

<!-- 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>

Todoフッター

<!-- 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 コンポーネントの機能は、テキスト ボックスにコンテンツを入力し、Enter キーを押すとカスタム イベントが送信され、カスタム イベントは文字列を渡し、最後にテキスト ボックスをクリアします。

テストケースを書く

まずテスト ケースを作成します。ファイルの場所には近接性の原則が推奨されます。

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>
    <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>

スタイルがデバッグされない限り、プロセス全体でこのコンポーネントの関数開発を完了するためにブラウザを開く必要はなく、ほとんどの場合、開発された関数は正しい必要があることがわかります。

コンポーネントの開発が完了し、テストに合格した後、コードを 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 = null

  beforeEach(() => {
    
    
    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)
    // 断言 class
    expect(todoItem.classes('completed')).toBeFalsy()
  })
})

コンポーネントで、必須フィールドをTodoAppsubcomponent に渡します。そうしないと、テストでエラーが報告されます。TodoItemprop

<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 = null

  beforeEach(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 () => {
    
    
    // 正向测试 传递一个真实的 id
    await wrapper.vm.handleDeleteTodo(1)
    expect(wrapper.vm.todos.length).toBe(2)
    expect(wrapper.findAllComponents(TodoItem).length).toBe(2)
  })

  test('Delete Todo', async () => {
    
    
    // 反向测试 传递要给不存在的 id
    await 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 -->
        <TodoItem
          v-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')

  // 断言 class
  expect(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>
    <input
      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
    }
  },
  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>
    <input
      v-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
<input
  v-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
<TodoItem
  v-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
<input
  v-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
}

おすすめ

転載: blog.csdn.net/u012961419/article/details/123785959