Vue2 アプリケーション テスト学習 03 - TDD ケース (その他のテスト ケース、スナップショット テスト、構成コード カバレッジ統計、codecov アップロード カバレッジ統計レポート、Github Actions 自動テストと継続的インテグレーション)

TodoApp 切り替え すべて選択

すべてのタスク アイテムのステータスを変更するには、[すべて選択] ボタンをクリックします。ボタンのスタイルは、すべてのタスク アイテムの選択されたステータスに応じて変わります。

テストケースを書く

test('Toggle All', async () => {
    
    
  const toggleAll = wrapper.findComponent('input[data-testid="toggle-all"]')

  // 选中全选按钮
  await toggleAll.setChecked()

  // 断言所有的任务都被选中
  wrapper.vm.todos.forEach(todo => {
    
    
    expect(todo.done).toBeTruthy()
  })

  // 取消完成状态
  await toggleAll.setChecked(false)
  wrapper.vm.todos.forEach(todo => {
    
    
    expect(todo.done).toBeFalsy()
  })
})

test('Toggle All State', async () => {
    
    
  const toggleAll = wrapper.findComponent('input[data-testid="toggle-all"]')

  // 让所有任务都变成完成状态
  wrapper.vm.todos.forEach(todo => {
    
    
    todo.done = true
  })
  // 等待视图更新
  await wrapper.vm.$nextTick()
  // 断言 toggleAll 选中
  expect(toggleAll.element.checked).toBeTruthy()

  // 取消某个任务未完成,断言 toggleAll 未选中
  wrapper.vm.todos[0].done = false
  await wrapper.vm.$nextTick()
  expect(toggleAll.element.checked).toBeFalsy()

  // 当没有任务的时候,断言 toggleAll 未选中
  await wrapper.setData({
    
    
    todos: [],
  })
  expect(toggleAll.element.checked).toBeFalsy()
})

コンポーネントの機能向上

// template
<input
  id="toggle-all"
  v-model="toggleAll"
  data-testid="toggle-all"
  class="toggle-all"
  type="checkbox"
/>

// js
computed: {
    
    
  toggleAll: {
    
    
    get() {
    
    
      // 获取 toggleAll 的选中状态
      return this.todos.length && this.todos.every(t => t.done)
    },
    set(checked) {
    
    
      this.todos.forEach(todo => {
    
    
        todo.done = checked
      })
    },
  },
},

TodoFooter の未処理のタスクの数

テストケースを書く

// src\components\TodoApp\__tests__\TodoFooter.js
import {
    
     shallowMount } from '@vue/test-utils'
import TodoFooter from '@/components/TodoApp/TodoFooter'

describe('TodoFooter.js', () => {
    
    
  /** @type {import('@vue/test-utils').Wrapper} */
  let wrapper = null

  beforeEach(async () => {
    
    
    const todos = [
      {
    
     id: 1, text: 'eat', done: false },
      {
    
     id: 2, text: 'play', done: true },
      {
    
     id: 3, text: 'sleep', done: false },
    ]
    wrapper = shallowMount(TodoFooter, {
    
    
      propsData: {
    
    
        todos,
      },
    })
  })

  test('Done Todos Count', () => {
    
    
    const count = wrapper.vm.todos.filter(t => !t.done).length
    const countEl = wrapper.findComponent('[data-testid="done-todos-count"]')

    expect(Number.parseInt(countEl.text())).toBe(count)
  })
})

コンポーネントの機能向上

<!-- src\components\TodoApp\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: Array,
      required: true,
    },
  },
  computed: {
    
    
    doneTodosCount() {
    
    
      return this.todos.filter(t => !t.done).length
    },
  },
}
</script>

必須の属性が TodoApp コンポーネントの TodoFooter コンポーネントに渡されることに注意してください。<TodoFooter :todos="todos" />

TodoFooterは完了タスクボタンの表示状態をクリアします

テストケースを書く

test('Clear Completed Show', () => {
    
    
  // beforeEach 中初始化的数据是 props
  // 而 props 是不能被子组件直接修改的
  // 所以这里要单独初始化数据
  const todos = [
    {
    
     id: 1, text: 'eat', done: false },
    {
    
     id: 2, text: 'play', done: false },
    {
    
     id: 3, text: 'sleep', done: false },
  ]
  wrapper = shallowMount(TodoFooter, {
    
    
    propsData: {
    
    
      todos,
    },
  })

  const button = wrapper.findComponent('[data-testid="clear-completed"]')

  expect(button.exists()).toBeFalsy()
})

コンポーネントの機能向上

// template
<button
  v-if="isClearCompletedShow"
  data-testid="clear-completed"
  class="clear-completed"
  @click="$emit('clear-completed')"
>
  Clear completed
</button>

// js
computed: {
    
    
  ...
  isClearCompletedShow() {
    
    
    return this.todos.some(t => t.done)
  },
},

完了したタスクをクリアする

Todoフッター

テストケースを書く

test('Clear Completed', async () => {
    
    
  const button = wrapper.findComponent('[data-testid="clear-completed"]')

  await button.trigger('click')

  expect(wrapper.emitted()['clear-completed']).toBeTruthy()
})

コンポーネントの機能向上

// template
<button
  v-if="isClearCompletedShow"
  data-testid="clear-completed"
  class="clear-completed"
  @click="$emit('clear-completed')"
>
  Clear completed
</button>

TodoApp

テストケースを書く

test('Clear All Completed', async () => {
    
    
  wrapper.vm.handleClearCompleted()
  await wrapper.vm.$nextTick()

  expect(wrapper.vm.todos).toEqual([
    {
    
     id: 1, text: 'eat', done: false },
    {
    
     id: 3, text: 'sleep', done: false },
  ])
})

コンポーネントの機能向上

// template
<TodoFooter
  :todos="todos"
  @clear-completed="handleClearCompleted"
/>

// js
methods: {
    
    
  ...
  handleClearCompleted() {
    
    
    // 清除所有已完成的任务项
    this.todos = this.todos.filter(t => !t.done)
  },
},

TodoApp データのフィルタリング ($route)

ルーティング パスに基づいてタスク項目をフィルターするには、Vue Router とともに使用する必要があります。

  • ローカル Vue (localVue) を作成し、その Vue-router を登録し、このローカル Vue に対してのみ有効にすることができます。
    • ただし、これにより外部依存関係 (vue-router インスタンス) が増加し、各テストで vue-router をロードする必要があり、パフォーマンスが低下します。
  • $routeフェイク(モック)することをお勧めします$router

コンポーネントをルートとして構成する

<!-- src\App.vue -->
<template>
  <div id="app">
    <!-- <TodoApp /> -->
    <<router-view />
  </div>
</template>

<script>
// import TodoApp from '@/components/TodoApp'

export default {
  name: 'App',
  // components: { TodoApp },
}
</script>

// src\router\index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import TodoApp from '@/components/TodoApp'

Vue.use(VueRouter)

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

const router = new VueRouter({
    
    
  routes,
})

export default router

テストケースを書く

beforeEach(async () => {
    
    
  const $route = {
    
    
    path: '/',
  }

  wrapper = shallowMount(TodoApp, {
    
    
    mocks: {
    
    
    // 伪造 $route
      $route,
    },
  })

  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('Filter Todos', async () => {
    
    
  // 将路由导航到 /
  wrapper.vm.$route.path = '/'
  await wrapper.vm.$nextTick()
  // 断言 filterTodos = 所有的任务
  expect(wrapper.vm.filterTodos).toEqual([
    {
    
     id: 1, text: 'eat', done: false },
    {
    
     id: 2, text: 'play', done: true },
    {
    
     id: 3, text: 'sleep', done: false },
  ])

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

  // 将路由导航到 /completed
  wrapper.vm.$route.path = '/completed'
  await wrapper.vm.$nextTick()
  // 断言 filterTodos = 所有的已完成任务
  expect(wrapper.vm.filterTodos).toEqual([
    {
    
     id: 2, text: 'play', done: true },
  ])
})

コンポーネントの機能向上

// template
<TodoItem
  v-for="todo in filterTodos"
  :key="todo.id"
  :todo="todo"
  @delete-todo="handleDeleteTodo"
  @edit-todo="handleEditTodo"
/>

// js
computed: {
    
    
  ...
  // 过滤数据
  filterTodos() {
    
    
    // 获取路由路径
    const path = this.$route.path

    // 根据路由路径过滤数据
    switch (path) {
    
    
      // 所有未完成任务
      case '/active':
        return this.todos.filter(t => !t.done)
      // 所有已完成任务
      case '/completed':
        return this.todos.filter(t => t.done)
      // 所有任务列表
      default:
        return this.todos
    }
  },
},

TodoFooter はナビゲーション リンク (ルーターリンク) を強調表示します。

ナビゲーションのハイライト機能を改善

ルーティング ナビゲーションを使用したいため、最初に関数を実装してからテストを作成できます。

<ul class="filters">
  <li>
    <router-link to="/" exact>All</router-link>
  </li>
  <li>
    <router-link to="/active">Active</router-link>
  </li>
  <li>
    <router-link to="/completed">Completed</router-link>
  </li>
</ul>

ルートの強調表示スタイルを設定します。

// src\router\index.js
...
const router = new VueRouter({
    
    
  routes,
  linkActiveClass: 'selected',
})

export default router

テストケースを書く

TodoFooter コンポーネントが<router-link>コンポーネントを使用するようになったので、Vue Router を導入する必要があります。導入しないと、テストの実行時にコンポーネントが登録されていないというエラーが報告されます。

テストの動作では、強調表示する必要があるナビゲーション リンクのみがselectedクラス名を持つようにすることができます。

// src\components\TodoApp\__tests__\TodoFooter.js
import {
    
     shallowMount, createLocalVue, mount } from '@vue/test-utils'
import TodoFooter from '@/components/TodoApp/TodoFooter'
import VueRouter from 'vue-router'

// 创建局部 Vue
const localVue = createLocalVue()
// 为局部 Vue 注册 VueRouter,不影响其他 Vue
localVue.use(VueRouter)
const router = new VueRouter({
    
    
  linkActiveClass: 'selected',
})

describe('TodoFooter.js', () => {
    
    
  /** @type {import('@vue/test-utils').Wrapper} */
  let wrapper = null

  beforeEach(async () => {
    
    
    const todos = [
      {
    
     id: 1, text: 'eat', done: false },
      {
    
     id: 2, text: 'play', done: true },
      {
    
     id: 3, text: 'sleep', done: false },
    ]

    // 注意:使用原来的 shallowMount 不会渲染 router-link 子组件
    // 这里需改用 mount
    wrapper = mount(TodoFooter, {
    
    
      propsData: {
    
    
        todos,
      },
      // 挂载局部 Vue 和 router
      localVue,
      router,
    })
  })

  test('Done Todos Count', () => {
    
    
    ...
  })

  test('Clear Completed Show', () => {
    
    
    ...
    wrapper = shallowMount(TodoFooter, {
    
    
      propsData: {
    
    
        todos,
      },
      // 挂载局部 Vue 和 router
      localVue,
      router,
    })

    ...
  })

  test('Clear Completed', async () => {
    
    
    ...
  })

  test('Router Link ActiveClass', async () => {
    
    
    // findAllComponents 返回 WrapperArray,它并不是一个数组类型
    // 需要使用内部方法来访问
    const links = wrapper.findAllComponents({
    
     name: 'RouterLink' })

    // 切换路由
    router.push('/completed')
    await localVue.nextTick()

    for (let i = 0; i < links.length; i++) {
    
    
      const link = links.at(i)
      if (link.vm.to === '/completed') {
    
    
        expect(link.classes()).toContain('selected')
      } else {
    
    
        expect(link.classes()).not.toContain('selected')
      }
    }

    // 切换路由
    router.push('/active')
    await localVue.nextTick()

    for (let i = 0; i < links.length; i++) {
    
    
      const link = links.at(i)
      if (link.vm.to === '/active') {
    
    
        expect(link.classes()).toContain('selected')
      } else {
    
    
        expect(link.classes()).not.toContain('selected')
      }
    }

    // 切换路由
    router.push('/')
    await localVue.nextTick()

    for (let i = 0; i < links.length; i++) {
    
    
      const link = links.at(i)
      if (link.vm.to === '/') {
    
    
        expect(link.classes()).toContain('selected')
      } else {
    
    
        expect(link.classes()).not.toContain('selected')
      }
    }
  })
})

スナップショットテスト

このアプリケーションのビジネス機能は基本的に開発されたので、これらのコンポーネントの開発が比較的安定したら (スタイルと構造を大幅に変更する必要がない)、スナップショット テストをコンポーネントに追加して、 UI 構造は適時にテストされます。

テストケースを書く

// src\components\TodoApp\__tests__\TodoHeader.js
import {
    
     shallowMount } from '@vue/test-utils'
import TodoHeader from '@/components/TodoApp/TodoHeader'

describe('TodoHeader.vue', () => {
    
    
  // 将渲染组件放到 beforeEach
  let wrapper = null

  beforeEach(() => {
    
    
    wrapper = shallowMount(TodoHeader)
  })

  test('New todo', async () => {
    
    
    // 可以给元素添加一个专门用于测试的 `data-testid`,方便测试的时候获取这个元素
    const input = wrapper.findComponent('input[data-testid="new-todo"]')
    const text = 'play'

    ...
  })

  test('snapshot', () => {
    
    
    expect(wrapper.html()).toMatchSnapshot()
  })
})

__snapshots__デフォルトでは、スナップショット テストが書き込まれるテスト ファイルが保存されるディレクトリの下に、スナップショット ファイルを保存するフォルダーが作成されます (例: ) src\components\TodoApp\__tests__\__snapshots__\TodoHeader.js.snap

以下の他のコンポーネントにスナップショット テストを追加します。

// src\components\TodoApp\__tests__\TodoApp.js
// src\components\TodoApp\__tests__\TodoFooter.js
// src\components\TodoApp\__tests__\TodoItem.js

// 添加
test('snapshot', () => {
    
    
  expect(wrapper.html()).toMatchSnapshot()
})

スナップショットを更新する

モニター モードでは、スナップショット テストが失敗し、変更が意図的である場合、uコマンドを使用してすべてのスナップショット ファイルを更新できます。

また、iこのコマンドを使用して対話型スナップショット モードに入ることができます。これにより、テスト ケースが再実行され、各テストの失敗に対処する方法が尋ねられ、次のテストが実行されます。

  • u現在のテストのスナップショット ファイルを更新します
  • s現在のテストをスキップする
  • qインタラクティブスナップショットモードを終了する
  • Enter現在のテストを再実行する
  • rインタラクティブスナップショットモードを再実行します

コードカバレッジ統計の構成

テストされていないコードが存在する可能性があり、実行プロセス中にバグが発生する可能性があるため、テスト カバレッジ率を 80% 以上に維持することが最善です。

開発中のリアルタイムの統計消費を避けるために、Jest テストを実行する新しいスクリプトを追加し、コマンド パラメーターを使用して統計コード カバレッジを指定することをお勧めします。

"scripts": {
    
    
  ...
  "coverage": "vue-cli-service test:unit --coverage"
},

npm run coverageスクリプトを実行すると、統計結果がコマンド ラインに出力されます。

ここに画像の説明を挿入

そして、プロジェクトのルート ディレクトリにカバレッジ レポートを保存するcoverageフォルダーを生成します。このフォルダーはcoverage\lcov-report\index.htmlページ上で開いて表示できます。

ここに画像の説明を挿入

ここに画像の説明を挿入

ここに画像の説明を挿入

ifTodoHeader コンポーネントにはテストされていないブランチがあり、テスト ケースを補足していることがわかります。

// src\components\TodoApp\__tests__\TodoHeader.js
test('New todo with empty text', async () => {
    
    
  const input = wrapper.findComponent('input[data-testid="new-todo"]')
  const text = ''
  await input.setValue(text)
  await input.trigger('keyup.enter')
  // 断言不会对外发布自定义事件
  expect(wrapper.emitted()['new-todo']).toBeFalsy()
})

npm run coverage統計テスト カバレッジを再統計し、レポート ページを更新して TodoHeader のカバレッジを再度表示します。

テストカバレッジを codecov にアップロードする

一般に、テスト カバレッジ レポートをプロジェクト リポジトリに保存することは推奨されません。.gitignore無視するcoverageディレクトリを次の場所に追加します。

# .gitignore
coverage

レポートは、 Codecovなどの専門的なレポート分析プラットフォームにアップロードできます

Codecov公式 Web サイトを開き、Github アカウントをバインドしてログインし、テスト カバレッジを表示するウェアハウスを選択します。

注: レポートをアップロードするためのプロジェクト git は、選択された git ウェアハウスである必要があります。そうでない場合、アップロード コマンドはエラーを報告しませんが、表示のために codecov プラットフォームにアップロードされません。

ここに画像の説明を挿入

Codecov トークンをコピーします (レポートをアップロードしていないウェアハウスにはデフォルトでスタート ガイドが表示され、ステップ 2 にトークンがあります。レポートをアップロードしたウェアハウスは [設定] パネルからトークンをコピーできます)。

ここに画像の説明を挿入

次に、Codecov をインストールします。

npm i -D codecov
# 或者安装到全局
# npm i -g codecov

テストカバレッジレポートを生成します。

# coverage 是运行 `jest -- coverrage` 的脚本 
npm run coverage

テスト カバレッジ レポートを codecov にアップロードします。

# 运行项目安装的 codecov 上传报告
npx codecov --token=xxx
# 使用全局安装的 codecov
codecov --token=xxx

Codecov に再度アクセスして、各コンポーネントのテスト カバレッジを含むレポート分析を確認します。

ここに画像の説明を挿入

[設定] パネルのバッジ リンクをコピーするREADME.mdと、codecov バッジが表示され、テスト カバレッジが表示され、アプリケーションが安全で信頼できるかどうかを他の開発者に知らせることができます。

ここに画像の説明を挿入

効果は以下の通りです

ここに画像の説明を挿入

自動テストと継続的統合

このプロジェクトでは、継続的統合のために Github Actions を使用します。

Github アクションの構成

プロジェクトのルート ディレクトリに新しいディレクトリとファイルを作成します.github/workflows/main.yml

# .github\workflows\main.yml
name: Publish And Deploy Demo

on:
  # 当提交 main 分支的代码的时候触发 action
  push:
    branches:
      - main
  # 或对 main 分支进行 pull request 的时候
  pull_request:
    branches:
      - main

jobs:
  build-and-deploy:
    # 运行环境
    runs-on: ubuntu-latest
    steps:
      # 下载仓库源码
      - name: Checkout
        uses: actions/checkout@main

      # 安装依赖 & 运行测试并生成覆盖率报告 & 项目打包
      - name: Install and Build
        run: |
          npm install
          npm run coverage
          npm run build

      # 发布到 GitHub Pages
      - name: Deploy
        uses: JamesIves/github-pages-deploy-[email protected]
        with:
          branch: gh-pages # The branch the action should deploy to.
          folder: dist # The folder the action should deploy.

      # 上传测试覆盖率报告到 codecov
      - name: codecov
        # 使用 codecov 官方提供的 action
        uses: codecov/codecov-action@v1
        with:
          token: ${
    
    {
    
     secrets.CODECOV_TOKEN }}

テストが失敗すると、Github Pages のデプロイやカバレッジ レポートのアップロードが行われずに、自動ビルドが中断されます。

Github が codecov トークンを保存するための環境変数を追加

ここに画像の説明を挿入

パッケージ化パスを変更する

Github Pages のアクセス アドレスには、デフォルトで第 2 レベルのドメイン名 (ウェアハウス名) が付けられます。たとえばhttp://xxx.github.io/vue-testing-demo/、パッケージ化パスを変更する必要があります。

// vue.config.js
const {
    
     defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
    
    
  transpileDependencies: true,
  // 添加
  publicPath: '/vue-testing-demo'
})

コードを送信する

プッシュコード、トリガーアクション

ここに画像の説明を挿入

ここに画像の説明を挿入

正常に実行されたら、Codecov にアクセスしてカバレッジ レポートを表示できます。

Github ページをホストするフォークを指定します

ここに画像の説明を挿入

変更が完了したら、再プッシュして Github Action の自動構築をトリガーし、Github Pages を公開する必要があります。

ワークフローステータスバッジを追加する

Github Actions ステータス バッジを追加し、README.md へのリンクを追加します。![example workflow](https://github.com/<OWNER>/<REPOSITORY>/actions/workflows/<WORKFLOW_FILE>/badge.svg)

  • <WORKFLOW_FILE>:.github/workflows/ディレクトリ内の.ymlワークフロー ファイル名。
  • <OWNER>github組織名
  • <REPOSITORY>:倉庫名

現在のプロジェクトに対応するものは次のとおりです。![](https://github.com/<你的 github 用户名>/vue-testing-demo/actions/workflows/main.yml/badge.svg)

効果:

ここに画像の説明を挿入

おすすめ

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