<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>Unit Test Framework｜林鼎淵 Dean Lin</title>
    <link>https://deanlin.net/tags/unit-test-framework/</link>
    <image><url>https://deanlin.net/images/author_hu_aa256fc934e9a932.jpg</url><title>Unit Test Framework｜林鼎淵 Dean Lin</title><link>https://deanlin.net/tags/unit-test-framework/</link><width>144</width><height>144</height></image>
    <description>林鼎淵（Dean Lin）的部落格：生成式 AI 企業內訓講師、外商資安公司軟體專家、7 本書作者。400 多篇文章用白話分享生成式 AI 實戰、Claude Code 與 Vibe Coding 教學、團隊合作與職場成長。</description>
    <generator>Hugo</generator>
    <language>zh-tw</language>
    <copyright>© 2020–2026 林鼎淵 Dean Lin</copyright>
    <lastBuildDate>Thu, 19 Jan 2023 13:58:00 +0800</lastBuildDate>
    <atom:link href="https://deanlin.net/tags/unit-test-framework/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>在 Vuetify（Vite &amp; TypeScript）中導入單元測試 Unit Testing</title>
      <link>https://deanlin.net/blog/202301/vuetify-vite-unit-testing/</link>
      <pubDate>Thu, 19 Jan 2023 13:58:00 +0800</pubDate>
      <guid>https://deanlin.net/blog/202301/vuetify-vite-unit-testing/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>這篇文章會先帶大家建立 Vuetify 的初始環境（Vue3 & Vite & TypeScript），並導入「Vue Test Utils」這款測試工具。</description>
      <media:content url="https://deanlin.net/blog/202301/vuetify-vite-unit-testing/images/vuetify-vite-unit-testing-og_hu_c3095805906bba3.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<blockquote>
<p>如果你完全沒有接觸過單元測試，建議可以先閱讀這兩篇文章：「<a href="https://deanlin.net/blog/202301/jest-unit-testing-typescript/">認識 Jest 測試框架，開始動手寫單元測試</a>、<a href="https://deanlin.net/blog/202301/jest-advanced-mock-setup-teardown/">Jest 框架進階應用（describe、Setup and Teardown、Mock function） </a>」。</p>
</blockquote>
<p>這篇文章會先帶大家建立 Vuetify 的初始環境（Vue3 &amp; Vite &amp; TypeScript），並導入「Vue Test Utils」這款測試工具。</p>
<p><img src="https://deanlin.net/blog/202301/vuetify-vite-unit-testing/images/vuetify-vite-unit-testing-01.webp" width="1148" height="598" alt="在 Vuetify（Vite &amp; TypeScript）中導入單元測試 Unit Testing（圖 1）" fetchpriority="high" decoding="async"></p>
<pre tabindex="0"><code>大綱

ㄧ、初始化專案、安裝必要套件
二、編寫測試的 config 檔
三、撰寫第一隻測試程式
四、運行測試程式確認結果符合預期
五、VSCode 的 Vitest 外掛
六、一些建議
</code></pre><blockquote>
<p>讀者可以直接到<a href="https://github.com/dean9703111/vuetify-vite-ts-test">筆者的 Github </a>，將專案 Clone 下來跑跑看，會更好理解裏面的邏輯（順手按個 ⭐ 更好 🤩）。</p>
</blockquote>
<h2 id="ㄧ初始化專案安裝必要套件">ㄧ、初始化專案、安裝必要套件<a class="h-anchor" href="#%e3%84%a7%e5%88%9d%e5%a7%8b%e5%8c%96%e5%b0%88%e6%a1%88%e5%ae%89%e8%a3%9d%e5%bf%85%e8%a6%81%e5%a5%97%e4%bb%b6" aria-label="這個段落的連結"></a></h2>
<blockquote>
<p>筆者 node.js 的版本為 18.13.0，建議選擇較新的版本，否則安裝套件的過程會遇到一些問題。</p>
</blockquote>
<p>這邊我們參考 Vuetify 官方文件的指令 <code>yarn create vuetify</code> 來建立專案。</p>
<p>建立過程中會問你很多問題，筆者的選擇如下：</p>
<ul>
<li><strong>Project name</strong>：自行決定（ex：vuetify-vite-ts-test）</li>
<li><strong>Which preset would you like to install</strong>：Essentials (Vuetify, VueRouter, Pinia)</li>
<li><strong>Use TypeScript</strong>：Yes</li>
<li><strong>Would you like to install dependencies with yarn, npm, or pnpm?</strong>：Yarn</li>
</ul>
<blockquote>
<p>不過問題時常調整，大家參考就好。</p>
</blockquote>
<p><img src="https://deanlin.net/blog/202301/vuetify-vite-unit-testing/images/vuetify-vite-unit-testing-02.webp" width="1400" height="898" alt="ㄧ、初始化專案、安裝必要套件（圖 2）" loading="lazy" decoding="async"></p>
<p>建立好 Vuetify 專案後，我們來安裝測試輔助套件如下：</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>yarn add --dev @vue/test-utils @vitejs/plugin-vue vitest jsdom
</span></span></code></pre></div><p>完成以上步驟後，在終端機輸入指令 <code>yarn dev</code>，看看是否能順利啟動。</p>
<p><img src="https://deanlin.net/blog/202301/vuetify-vite-unit-testing/images/vuetify-vite-unit-testing-03.webp" width="489" height="195" alt="ㄧ、初始化專案、安裝必要套件（圖 3）" loading="lazy" decoding="async"></p>
<p><img src="https://deanlin.net/blog/202301/vuetify-vite-unit-testing/images/vuetify-vite-unit-testing-04.webp" width="1400" height="795" alt="ㄧ、初始化專案、安裝必要套件（圖 4）" loading="lazy" decoding="async"></p>
<p>如果網頁能順利跑出來就恭喜你完成第一步嚕！</p>
<hr>
<h2 id="二編寫測試的-config-檔">二、編寫測試的 config 檔<a class="h-anchor" href="#%e4%ba%8c%e7%b7%a8%e5%af%ab%e6%b8%ac%e8%a9%a6%e7%9a%84-config-%e6%aa%94" aria-label="這個段落的連結"></a></h2>
<p>在專案根目錄下，新增 vitest.config.ts，這是專門為了單元測試寫的相關設定檔，你仔細看會發現它跟「vite.config.ts」設定很像。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#ff7b72">import</span> { defineConfig } <span style="color:#ff7b72">from</span> <span style="color:#a5d6ff">&#39;vitest/config&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">import</span> vue <span style="color:#ff7b72">from</span> <span style="color:#a5d6ff">&#39;@vitejs/plugin-vue&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">import</span> vuetify <span style="color:#ff7b72">from</span> <span style="color:#a5d6ff">&#39;vite-plugin-vuetify&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">import</span> { fileURLToPath, URL } <span style="color:#ff7b72">from</span> <span style="color:#a5d6ff">&#39;node:url&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">export</span> <span style="color:#ff7b72">default</span> defineConfig({
</span></span><span style="display:flex;"><span>  <span style="color:#8b949e;font-style:italic">// Resolver
</span></span></span><span style="display:flex;"><span>  resolve<span style="color:#ff7b72;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// https://vitejs.dev/config/#resolve-alias
</span></span></span><span style="display:flex;"><span>    alias<span style="color:#ff7b72;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#a5d6ff">&#39;@&#39;</span><span style="color:#ff7b72;font-weight:bold">:</span> fileURLToPath(<span style="color:#ff7b72">new</span> URL(<span style="color:#a5d6ff">&#39;./src&#39;</span>, <span style="color:#ff7b72">import</span>.meta.url)),
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>  <span style="color:#8b949e;font-style:italic">// plugins
</span></span></span><span style="display:flex;"><span>  plugins<span style="color:#ff7b72;font-weight:bold">:</span> [
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// Vue3
</span></span></span><span style="display:flex;"><span>    vue(),
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// Vuetify Loader
</span></span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// https://github.com/vuetifyjs/vuetify-loader
</span></span></span><span style="display:flex;"><span>    vuetify({
</span></span><span style="display:flex;"><span>      autoImport: <span style="color:#ff7b72">true</span>,
</span></span><span style="display:flex;"><span>      styles<span style="color:#ff7b72;font-weight:bold">:</span> { configFile<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;./src/styles/variables.scss&#39;</span> },
</span></span><span style="display:flex;"><span>    }),
</span></span><span style="display:flex;"><span>  ],
</span></span><span style="display:flex;"><span>  test<span style="color:#ff7b72;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// https://vitest.dev/guide/#configuring-vitest
</span></span></span><span style="display:flex;"><span>    globals: <span style="color:#ff7b72">true</span>,
</span></span><span style="display:flex;"><span>    environment<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;jsdom&#39;</span>,
</span></span><span style="display:flex;"><span>    deps<span style="color:#ff7b72;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>      inline<span style="color:#ff7b72;font-weight:bold">:</span> [<span style="color:#a5d6ff">&#39;vuetify&#39;</span>],
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><hr>
<h2 id="三撰寫第一隻測試程式">三、撰寫第一隻測試程式<a class="h-anchor" href="#%e4%b8%89%e6%92%b0%e5%af%ab%e7%ac%ac%e4%b8%80%e9%9a%bb%e6%b8%ac%e8%a9%a6%e7%a8%8b%e5%bc%8f" aria-label="這個段落的連結"></a></h2>
<p>我們就以入口頁「HelloWorld.vue」為範例跟大家說明。</p>
<h3 id="-調整srccomponentshelloworldvue的程式">調整「src/components/HelloWorld.vue」的程式<a class="h-anchor" href="#-%e8%aa%bf%e6%95%b4srccomponentshelloworldvue%e7%9a%84%e7%a8%8b%e5%bc%8f" aria-label="這個段落的連結"></a></h3>
<p>這裡我們新增 defineProps 以接收傳進來的「msg」，並在頁面上顯示。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span>&lt;<span style="color:#7ee787">template</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#7ee787">v</span><span style="color:#f85149">-</span>container class<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;fill-height&#34;</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#7ee787">v</span><span style="color:#f85149">-</span>responsive class<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;d-flex align-center text-center fill-height&#34;</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#7ee787">v</span><span style="color:#f85149">-</span>img
</span></span><span style="display:flex;"><span>        contain
</span></span><span style="display:flex;"><span>        height<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;200&#34;</span>
</span></span><span style="display:flex;"><span>        src<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;@/assets/logo.svg&#34;</span>
</span></span><span style="display:flex;"><span>      /&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#7ee787">h1</span> class<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;green&#34;</span>&gt;{{ msg }}&lt;/<span style="color:#7ee787">h1</span>&gt;
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72;font-weight:bold">&lt;</span><span style="color:#f85149">/v-responsive&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72;font-weight:bold">&lt;</span><span style="color:#f85149">/v-container&gt;</span>
</span></span><span style="display:flex;"><span>&lt;/<span style="color:#7ee787">template</span>&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>&lt;<span style="color:#7ee787">script</span> setup lang<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;ts&#34;</span>&gt;
</span></span><span style="display:flex;"><span>defineProps<span style="color:#ff7b72;font-weight:bold">&lt;</span>{
</span></span><span style="display:flex;"><span>  msg: <span style="color:#ff7b72">string</span>;
</span></span><span style="display:flex;"><span>}<span style="color:#ff7b72;font-weight:bold">&gt;</span>();
</span></span><span style="display:flex;"><span>&lt;/<span style="color:#7ee787">script</span>&gt;
</span></span></code></pre></div><h3 id="-新增srccomponents__test__helloworldspects作為測試程式">新增「src/components/__test__/HelloWorld.spec.ts」作為測試程式<a class="h-anchor" href="#-%e6%96%b0%e5%a2%9esrccomponents__test__helloworldspects%e4%bd%9c%e7%82%ba%e6%b8%ac%e8%a9%a6%e7%a8%8b%e5%bc%8f" aria-label="這個段落的連結"></a></h3>
<p>在 components 資料夾下建立「__test__」的資料夾放置測試程式，我們透過它來確認 HelloWorld 頁面是否有收到 props 的 msg，並如預期被渲染。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#ff7b72">import</span> { describe, it, expect } <span style="color:#ff7b72">from</span> <span style="color:#a5d6ff">&#34;vitest&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">import</span> { mount } <span style="color:#ff7b72">from</span> <span style="color:#a5d6ff">&#34;@vue/test-utils&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">import</span> HelloWorld <span style="color:#ff7b72">from</span> <span style="color:#a5d6ff">&#34;../HelloWorld.vue&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">import</span> vuetify <span style="color:#ff7b72">from</span> <span style="color:#a5d6ff">&#39;@/plugins/vuetify&#39;</span>;
</span></span><span style="display:flex;"><span>describe(<span style="color:#a5d6ff">&#34;HelloWorld&#34;</span>, () <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>  it(<span style="color:#a5d6ff">&#34;renders properly&#34;</span>, () <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> wrapper <span style="color:#ff7b72;font-weight:bold">=</span> mount(HelloWorld, {
</span></span><span style="display:flex;"><span>      <span style="color:#ff7b72">global</span><span style="color:#ff7b72;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>        plugins<span style="color:#ff7b72;font-weight:bold">:</span> [vuetify],
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      props<span style="color:#ff7b72;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>        msg<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;Hello Vitest&#34;</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    });
</span></span><span style="display:flex;"><span>    expect(wrapper.text()).toContain(<span style="color:#a5d6ff">&#34;Hello Vitest&#34;</span>);
</span></span><span style="display:flex;"><span>  });
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><hr>
<h2 id="四運行測試程式確認結果符合預期">四、運行測試程式確認結果符合預期<a class="h-anchor" href="#%e5%9b%9b%e9%81%8b%e8%a1%8c%e6%b8%ac%e8%a9%a6%e7%a8%8b%e5%bc%8f%e7%a2%ba%e8%aa%8d%e7%b5%90%e6%9e%9c%e7%ac%a6%e5%90%88%e9%a0%90%e6%9c%9f" aria-label="這個段落的連結"></a></h2>
<p>在 package.json 新增一條測試用的 script：<code>&ldquo;test:unit&rdquo;: &ldquo;vitest &ndash;environment jsdom &ndash;root src/&quot;</code></p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#7ee787">&#34;name&#34;</span>: <span style="color:#a5d6ff">&#34;vuetify-vite-ts-test&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#7ee787">&#34;version&#34;</span>: <span style="color:#a5d6ff">&#34;0.0.0&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#7ee787">&#34;private&#34;</span>: <span style="color:#79c0ff">true</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#7ee787">&#34;scripts&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#7ee787">&#34;dev&#34;</span>: <span style="color:#a5d6ff">&#34;vite&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#7ee787">&#34;build&#34;</span>: <span style="color:#a5d6ff">&#34;vue-tsc --noEmit &amp;&amp; vite build&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#7ee787">&#34;preview&#34;</span>: <span style="color:#a5d6ff">&#34;vite preview&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#7ee787">&#34;lint&#34;</span>: <span style="color:#a5d6ff">&#34;eslint . --fix --ignore-path .gitignore&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#7ee787">&#34;test:unit&#34;</span>: <span style="color:#a5d6ff">&#34;vitest --environment jsdom --root src/&#34;</span>
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>  <span style="color:#7ee787">&#34;dependencies&#34;</span>: {<span style="color:#f85149">...</span>},
</span></span><span style="display:flex;"><span>  <span style="color:#7ee787">&#34;devDependencies&#34;</span>: {<span style="color:#f85149">...</span>}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>新增後在終端機輸入指令 <code>yarn test:unit</code> 就會開始執行測試，如果看到下面的畫面，就恭喜你成功導入單元測試嚕！</p>
<p><img src="https://deanlin.net/blog/202301/vuetify-vite-unit-testing/images/vuetify-vite-unit-testing-05.webp" width="944" height="420" alt="四、運行測試程式確認結果符合預期（圖 5）" loading="lazy" decoding="async"></p>
<hr>
<h2 id="五vscode-的-vitest-外掛">五、VSCode 的 Vitest 外掛<a class="h-anchor" href="#%e4%ba%94vscode-%e7%9a%84-vitest-%e5%a4%96%e6%8e%9b" aria-label="這個段落的連結"></a></h2>
<p>跟 Jest 一樣，VSCode 也有 Vite 的外掛喔！他叫做 <a href="https://marketplace.visualstudio.com/items?itemName=ZixuanChen.vitest-explorer">Vitest</a>。</p>
<p><img src="https://deanlin.net/blog/202301/vuetify-vite-unit-testing/images/vuetify-vite-unit-testing-06.webp" width="1400" height="384" alt="五、VSCode 的 Vitest 外掛（圖 6）" loading="lazy" decoding="async"></p>
<p>安裝完成後，點擊 VSCode 左側的「Testing」icon，就可以測試嚕！</p>
<p><img src="https://deanlin.net/blog/202301/vuetify-vite-unit-testing/images/vuetify-vite-unit-testing-07.webp" width="1400" height="540" alt="五、VSCode 的 Vitest 外掛（圖 7）" loading="lazy" decoding="async"></p>
<hr>
<h2 id="六一些建議">六、一些建議<a class="h-anchor" href="#%e5%85%ad%e4%b8%80%e4%ba%9b%e5%bb%ba%e8%ad%b0" aria-label="這個段落的連結"></a></h2>
<h3 id="-vscode-改使用-volar-這個外掛">VSCode 改使用 Volar 這個外掛<a class="h-anchor" href="#-vscode-%e6%94%b9%e4%bd%bf%e7%94%a8-volar-%e9%80%99%e5%80%8b%e5%a4%96%e6%8e%9b" aria-label="這個段落的連結"></a></h3>
<p>今天這個是以 Vue3 設計的專案，如果你過去 VSCode 使用的版本是 Vue2，那可能有些程式下方會有警告，那可能是 VSCode 過去在 Vue2 時選用 <a href="https://marketplace.visualstudio.com/items?itemName=octref.vetur">Vetur</a> 這個外掛，現在 Vue3 要改成使用 <a href="https://marketplace.visualstudio.com/items?itemName=vue.volar">Volar</a>。</p>
<h3 id="-如果遇到了type-plugin-is-not-assignable-to-type-pluginoption這類的錯誤">如果遇到了「Type &lsquo;Plugin&rsquo; is not assignable to type &lsquo;PluginOption&rsquo;」這類的錯誤<a class="h-anchor" href="#-%e5%a6%82%e6%9e%9c%e9%81%87%e5%88%b0%e4%ba%86type-plugin-is-not-assignable-to-type-pluginoption%e9%80%99%e9%a1%9e%e7%9a%84%e9%8c%af%e8%aa%a4" aria-label="這個段落的連結"></a></h3>
<p>通常是因為安裝了多個 Vite 的版本所導致的，建議先將根目錄下的「node_modules」以及「.lock」全部移除，然後再重新安裝套件。</p>
<blockquote>
<p>筆者是在 nvm 切換 node.js 版本時遇到這個問題的。</p>
</blockquote>
<p>那今天的文章就分享到這裡，<a href="https://deanlin.net/blog/202301/vue-test-utils-examples/">下一篇文章會以實際案例跟大家分享 Vue Test Utils 的應用</a>！</p>
<a class="linkcard" href="https://deanlin.net/series/frontend-testing/">
  <span class="lc-body">
    <span class="lc-title">前端單元測試</span>
    <span class="lc-desc">替前端專案導入測試：學習地圖、Jest 基礎與進階，再到 Vuetify（Vite &amp; TypeScript）中的 @vue/test-utils。</span>
    <span class="lc-site"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M5 12h14M13 6l6 6-6 6"/></svg>林鼎淵 Dean Lin</span>
  </span>
</a>

]]></content:encoded>
    </item>
    <item>
      <title>Jest 框架進階應用（describe、Setup and Teardown、Mock function）</title>
      <link>https://deanlin.net/blog/202301/jest-advanced-mock-setup-teardown/</link>
      <pubDate>Thu, 19 Jan 2023 10:55:58 +0800</pubDate>
      <guid>https://deanlin.net/blog/202301/jest-advanced-mock-setup-teardown/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>一個功能底下可能有很多函式，因此我們要對它進行分組（describe）；當我們想測試特定情境時，可以在測試前增加資料（Setup）並於測試後移除（Teardown）；為了讓單元測試更存粹，可以透過模擬函式（Mock function）減少相依性。</description>
      <media:content url="https://deanlin.net/blog/202301/jest-advanced-mock-setup-teardown/images/jest-advanced-mock-setup-teardown-og_hu_8dc9a72b055a1c4f.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<blockquote>
<p><em>如</em>果你對 Jest 這個測試框架還一無所知，建議先看<a href="https://deanlin.net/blog/202301/jest-unit-testing-typescript/">上一篇文章</a>喔！</p>
</blockquote>
<p>一個功能底下可能有很多函式，如果測試程式沒有適當的區分，那在遇到錯誤時會難以管理，因此我們要對它進行分組（describe）；當我們想測試特定情境時，可以在測試前增加資料（Setup）並於測試後移除（Teardown）；為了讓單元測試更存粹，可以透過模擬函式（Mock function）減少相依性。</p>
<p><img src="https://deanlin.net/blog/202301/jest-advanced-mock-setup-teardown/images/jest-advanced-mock-setup-teardown-01.webp" width="1150" height="597" alt="Jest 框架進階應用（describe、Setup and Teardown、Mock function）（圖 1）" fetchpriority="high" decoding="async"></p>
<pre tabindex="0"><code>大綱

一、測試分組（describe）
二、在測試前、後要執行的任務（Setup and Teardown）
三、用 Mock Function 減少測試時的相依性
四、VSCode 的 Jest 外掛
</code></pre><blockquote>
<p>讀者可以直接到<a href="https://github.com/dean9703111/jest-typescript-starter/tree/master/jest-advanced">筆者的 Github </a>，將專案 Clone 下來跑跑看，會更好理解裏面的邏輯（順手按個 ⭐ 更好 🤩）。</p>
</blockquote>
<h2 id="一測試分組describe">一、測試分組（describe）<a class="h-anchor" href="#%e4%b8%80%e6%b8%ac%e8%a9%a6%e5%88%86%e7%b5%84describe" aria-label="這個段落的連結"></a></h2>
<p>比如今天我們設計一個計算機，裏面包含「加減乘除」等功能，如果要針對不同 function 做分組測試，我們可以這樣做：</p>
<ul>
<li><strong>calculator.ts</strong>：計算機基礎功能（加減乘除）。</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#ff7b72">export</span> <span style="color:#ff7b72">function</span> sum(a: <span style="color:#ff7b72">number</span>, b: <span style="color:#ff7b72">number</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">return</span> a <span style="color:#ff7b72;font-weight:bold">+</span> b;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">export</span> <span style="color:#ff7b72">function</span> minus(a: <span style="color:#ff7b72">number</span>, b: <span style="color:#ff7b72">number</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">return</span> a <span style="color:#ff7b72;font-weight:bold">-</span> b;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">export</span> <span style="color:#ff7b72">function</span> multiply(a: <span style="color:#ff7b72">number</span>, b: <span style="color:#ff7b72">number</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">return</span> a <span style="color:#ff7b72;font-weight:bold">*</span> b;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">export</span> <span style="color:#ff7b72">function</span> divide(a: <span style="color:#ff7b72">number</span>, b: <span style="color:#ff7b72">number</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">return</span> a <span style="color:#ff7b72;font-weight:bold">/</span> b;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><ul>
<li><strong>calculator.test.ts</strong>：分組測試計算機基礎功能。</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#ff7b72">import</span> { sum, minus, multiply, divide } <span style="color:#ff7b72">from</span> <span style="color:#a5d6ff">&#34;./calculator&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>describe(<span style="color:#a5d6ff">&#34;測試加法&#34;</span>, () <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>  test(<span style="color:#a5d6ff">&#34;1 + 1 = 2&#34;</span>, () <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>    expect(sum(<span style="color:#a5d6ff">1</span>, <span style="color:#a5d6ff">1</span>)).toBe(<span style="color:#a5d6ff">2</span>);
</span></span><span style="display:flex;"><span>  });
</span></span><span style="display:flex;"><span>  test(<span style="color:#a5d6ff">&#34;3 + 3 = 7&#34;</span>, () <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>    expect(sum(<span style="color:#a5d6ff">3</span>, <span style="color:#a5d6ff">4</span>)).toBe(<span style="color:#a5d6ff">7</span>);
</span></span><span style="display:flex;"><span>  });
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>describe(<span style="color:#a5d6ff">&#34;測試減法&#34;</span>, () <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>  test(<span style="color:#a5d6ff">&#34;5 - 1 = 4&#34;</span>, () <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>    expect(minus(<span style="color:#a5d6ff">5</span>, <span style="color:#a5d6ff">1</span>)).toBe(<span style="color:#a5d6ff">4</span>);
</span></span><span style="display:flex;"><span>  });
</span></span><span style="display:flex;"><span>  test(<span style="color:#a5d6ff">&#34;3 - 3 = 0&#34;</span>, () <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>    expect(minus(<span style="color:#a5d6ff">3</span>, <span style="color:#a5d6ff">3</span>)).toBe(<span style="color:#a5d6ff">0</span>);
</span></span><span style="display:flex;"><span>  });
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>describe(<span style="color:#a5d6ff">&#34;測試乘法&#34;</span>, () <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>  test(<span style="color:#a5d6ff">&#34;1 * 5 = 5&#34;</span>, () <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>    expect(multiply(<span style="color:#a5d6ff">1</span>, <span style="color:#a5d6ff">5</span>)).toBe(<span style="color:#a5d6ff">5</span>);
</span></span><span style="display:flex;"><span>  });
</span></span><span style="display:flex;"><span>  test(<span style="color:#a5d6ff">&#34;3 * 3 = 9&#34;</span>, () <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>    expect(multiply(<span style="color:#a5d6ff">3</span>, <span style="color:#a5d6ff">3</span>)).toBe(<span style="color:#a5d6ff">9</span>);
</span></span><span style="display:flex;"><span>  });
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>describe(<span style="color:#a5d6ff">&#34;測試除法&#34;</span>, () <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>  test(<span style="color:#a5d6ff">&#34;6 / 2 = 3&#34;</span>, () <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>    expect(divide(<span style="color:#a5d6ff">6</span>, <span style="color:#a5d6ff">2</span>)).toBe(<span style="color:#a5d6ff">3</span>);
</span></span><span style="display:flex;"><span>  });
</span></span><span style="display:flex;"><span>  test(<span style="color:#a5d6ff">&#34;3 / 2 = 1.5&#34;</span>, () <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>    expect(divide(<span style="color:#a5d6ff">3</span>, <span style="color:#a5d6ff">2</span>)).toBe(<span style="color:#a5d6ff">1.5</span>);
</span></span><span style="display:flex;"><span>  });
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><p>運行測試指令 <code>npm run test</code>，順利的話就會看到下圖測試分組的結果嚕！</p>
<p><img src="https://deanlin.net/blog/202301/jest-advanced-mock-setup-teardown/images/jest-advanced-mock-setup-teardown-02.webp" width="946" height="1042" alt="一、測試分組（describe）（圖 2）" loading="lazy" decoding="async"></p>
<hr>
<h2 id="二在測試前後要執行的任務setup-and-teardown">二、在測試前、後要執行的任務（Setup and Teardown）<a class="h-anchor" href="#%e4%ba%8c%e5%9c%a8%e6%b8%ac%e8%a9%a6%e5%89%8d%e5%be%8c%e8%a6%81%e5%9f%b7%e8%a1%8c%e7%9a%84%e4%bb%bb%e5%8b%99setup-and-teardown" aria-label="這個段落的連結"></a></h2>
<p>Jest 有提供 4 種函數：</p>
<ul>
<li><strong>beforeAll</strong>：在所有測試開始前執行。</li>
<li><strong>afterAll</strong>：在所有測試完畢後執行。</li>
<li><strong>beforeEach</strong>：在每個測試開始前執行。</li>
<li><strong>afterEach</strong>：在每個測試結束後執行。</li>
</ul>
<p>舉個例子，如果今天要設計「考核系統」，除了有考生外，還需要模擬出「合格」、「不合格」的情境，為了避免影響到資料庫，我們可以這樣做：</p>
<ul>
<li><strong>user.ts</strong>：我們先預設的 score 的基準為 60 分，可透過 addScore、minusScore 來加減分。</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#ff7b72">export</span> <span style="color:#ff7b72">default</span> <span style="color:#ff7b72">class</span> user {
</span></span><span style="display:flex;"><span>  name: <span style="color:#ff7b72">string</span>;
</span></span><span style="display:flex;"><span>  score: <span style="color:#ff7b72">number</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">constructor</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">this</span>.name <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#34;&#34;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">this</span>.score <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">60</span>;
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>  addScore() {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">this</span>.score <span style="color:#ff7b72;font-weight:bold">+=</span> <span style="color:#a5d6ff">10</span>;
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>  minusScore() {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">this</span>.score <span style="color:#ff7b72;font-weight:bold">-=</span> <span style="color:#a5d6ff">10</span>;
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><ul>
<li><strong>exam.test.ts</strong>：利用「beforeAll」將考生的資料初始化，並透過「beforeEach」將分組測試的分數重新校正（若少了這個動作，第二個測試會沿用上一個測試的結果，會變成 70–10=60 分），測試完畢後用「afterAll」將考生資料清空。</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#ff7b72">import</span> user <span style="color:#ff7b72">from</span> <span style="color:#a5d6ff">&#34;./user&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> userDB: <span style="color:#ff7b72">user</span>[] <span style="color:#ff7b72;font-weight:bold">=</span> [];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> insertTestData <span style="color:#ff7b72;font-weight:bold">=</span> (name: <span style="color:#ff7b72">string</span>) <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> User <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> user();
</span></span><span style="display:flex;"><span>  User.name <span style="color:#ff7b72;font-weight:bold">=</span> name;
</span></span><span style="display:flex;"><span>  userDB.push(User);
</span></span><span style="display:flex;"><span>};
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> clearTestData <span style="color:#ff7b72;font-weight:bold">=</span> () <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>  userDB.length <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">0</span>;
</span></span><span style="display:flex;"><span>};
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>beforeAll(() <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>  insertTestData(<span style="color:#a5d6ff">&#34;baobao&#34;</span>);
</span></span><span style="display:flex;"><span>  insertTestData(<span style="color:#a5d6ff">&#34;mochi&#34;</span>);
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>describe(<span style="color:#a5d6ff">&#34;baoboa 分數計算&#34;</span>, () <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>  beforeEach(() <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>    userDB[<span style="color:#a5d6ff">0</span>].score <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">60</span>;
</span></span><span style="display:flex;"><span>  });
</span></span><span style="display:flex;"><span>  test(<span style="color:#a5d6ff">&#34;60 + 10 為 70 分&#34;</span>, () <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>    userDB[<span style="color:#a5d6ff">0</span>].addScore();
</span></span><span style="display:flex;"><span>    expect(userDB[<span style="color:#a5d6ff">0</span>].score).toBe(<span style="color:#a5d6ff">70</span>);
</span></span><span style="display:flex;"><span>  });
</span></span><span style="display:flex;"><span>  test(<span style="color:#a5d6ff">&#34;60 - 10 為 50 分&#34;</span>, () <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>    userDB[<span style="color:#a5d6ff">0</span>].minusScore();
</span></span><span style="display:flex;"><span>    expect(userDB[<span style="color:#a5d6ff">0</span>].score).toBe(<span style="color:#a5d6ff">50</span>);
</span></span><span style="display:flex;"><span>  });
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>describe(<span style="color:#a5d6ff">&#34;mochi 分數計算&#34;</span>, () <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>  beforeEach(() <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>    userDB[<span style="color:#a5d6ff">1</span>].score <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">60</span>;
</span></span><span style="display:flex;"><span>  });
</span></span><span style="display:flex;"><span>  test(<span style="color:#a5d6ff">&#34;60 + 10 為 70 分&#34;</span>, () <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>    userDB[<span style="color:#a5d6ff">1</span>].addScore();
</span></span><span style="display:flex;"><span>    expect(userDB[<span style="color:#a5d6ff">1</span>].score).toBe(<span style="color:#a5d6ff">70</span>);
</span></span><span style="display:flex;"><span>  });
</span></span><span style="display:flex;"><span>  test(<span style="color:#a5d6ff">&#34;60 - 10 為 50 分&#34;</span>, () <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>    userDB[<span style="color:#a5d6ff">1</span>].minusScore();
</span></span><span style="display:flex;"><span>    expect(userDB[<span style="color:#a5d6ff">1</span>].score).toBe(<span style="color:#a5d6ff">50</span>);
</span></span><span style="display:flex;"><span>  });
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>afterAll(() <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>  clearTestData();
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><p>這邊補充說明一下，運行單個測試檔案的指令為： <code>npm run test -t path/user.test.ts</code>。</p>
<p>如果運行順利，就會看到下圖的測試結果嚕！</p>
<p><img src="https://deanlin.net/blog/202301/jest-advanced-mock-setup-teardown/images/jest-advanced-mock-setup-teardown-03.webp" width="1232" height="624" alt="二、在測試前、後要執行的任務（Setup and Teardown）（圖 3）" loading="lazy" decoding="async"></p>
<hr>
<h2 id="三用-mock-function-減少測試時的相依性">三、用 Mock Function 減少測試時的相依性<a class="h-anchor" href="#%e4%b8%89%e7%94%a8-mock-function-%e6%b8%9b%e5%b0%91%e6%b8%ac%e8%a9%a6%e6%99%82%e7%9a%84%e7%9b%b8%e4%be%9d%e6%80%a7" aria-label="這個段落的連結"></a></h2>
<p>單元測試時，我們更關心「單一功能」的正確與否；如果一個單元測試涵蓋太多函式，那在發生錯誤時，我們很難判斷到底是哪一段發生錯誤。</p>
<ul>
<li><strong>fetch.js</strong>：我們透過「fetchData」呼叫外部 api，而「hideComplete」是設計用來隱藏已完成的 todos。</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#ff7b72">import</span> axios <span style="color:#ff7b72">from</span> <span style="color:#a5d6ff">&#34;axios&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">export</span> <span style="color:#ff7b72">async</span> <span style="color:#ff7b72">function</span> fetchData() {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> response <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> axios.<span style="color:#ff7b72">get</span>(<span style="color:#a5d6ff">`https://jsonplaceholder.typicode.com/todos`</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">return</span> response;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">export</span> <span style="color:#ff7b72">function</span> hideComplete(data) {
</span></span><span style="display:flex;"><span>  data <span style="color:#ff7b72;font-weight:bold">=</span> data.filter(<span style="color:#ff7b72">function</span> (value<span style="color:#ff7b72;font-weight:bold">:</span> { completed: <span style="color:#ff7b72">boolean</span> }) {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">return</span> value.completed <span style="color:#ff7b72;font-weight:bold">===</span> <span style="color:#79c0ff">false</span>;
</span></span><span style="display:flex;"><span>  });
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">return</span> data;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><ul>
<li><strong>fetch.test.js</strong>：為了減少相依性，我們不希望直接呼叫真實的 api，所以透過 <code>jest.mock</code> 將 axios 給 mock 起來；並透過 3 種情境測試：「有成功換成模擬資料、資料格式符合預期、獨立測試 hideComplete 函式」。</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">import</span> { fetchData, hideComplete } from <span style="color:#a5d6ff">&#34;./fetch&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">import</span> axios from <span style="color:#a5d6ff">&#34;axios&#34;</span>;
</span></span><span style="display:flex;"><span>jest.mock(<span style="color:#a5d6ff">&#34;axios&#34;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> mockedAxios <span style="color:#ff7b72;font-weight:bold">=</span> axios as jest.Mocked<span style="color:#ff7b72;font-weight:bold">&lt;</span><span style="color:#ff7b72">typeof</span> axios<span style="color:#ff7b72;font-weight:bold">&gt;</span>;
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// 模擬我們期望收到的資料結構
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> data <span style="color:#ff7b72;font-weight:bold">=</span> [
</span></span><span style="display:flex;"><span>  {
</span></span><span style="display:flex;"><span>    userId<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">1</span>,
</span></span><span style="display:flex;"><span>    id<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">1</span>,
</span></span><span style="display:flex;"><span>    title<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;task 1&#34;</span>,
</span></span><span style="display:flex;"><span>    completed<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#79c0ff">false</span>,
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>  {
</span></span><span style="display:flex;"><span>    userId<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">2</span>,
</span></span><span style="display:flex;"><span>    id<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">2</span>,
</span></span><span style="display:flex;"><span>    title<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;task 2&#34;</span>,
</span></span><span style="display:flex;"><span>    completed<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#79c0ff">true</span>,
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>test(<span style="color:#a5d6ff">&#34;確認結果被替換成模擬資料&#34;</span>, <span style="color:#ff7b72">async</span> () =&gt; {
</span></span><span style="display:flex;"><span>  mockedAxios.get.mockResolvedValue(data);
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> result <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> fetchData();
</span></span><span style="display:flex;"><span>  expect(result).toEqual(data);
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>test(<span style="color:#a5d6ff">&#34;驗證資料格式&#34;</span>, <span style="color:#ff7b72">async</span> () =&gt; {
</span></span><span style="display:flex;"><span>  mockedAxios.get.mockResolvedValue(data);
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> result <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> fetchData();
</span></span><span style="display:flex;"><span>  expect(<span style="color:#ff7b72">typeof</span> result[<span style="color:#a5d6ff">0</span>].userId).toBe(<span style="color:#a5d6ff">&#34;number&#34;</span>);
</span></span><span style="display:flex;"><span>  expect(<span style="color:#ff7b72">typeof</span> result[<span style="color:#a5d6ff">0</span>].id).toBe(<span style="color:#a5d6ff">&#34;number&#34;</span>);
</span></span><span style="display:flex;"><span>  expect(<span style="color:#ff7b72">typeof</span> result[<span style="color:#a5d6ff">0</span>].title).toBe(<span style="color:#a5d6ff">&#34;string&#34;</span>);
</span></span><span style="display:flex;"><span>  expect(<span style="color:#ff7b72">typeof</span> result[<span style="color:#a5d6ff">0</span>].completed).toBe(<span style="color:#a5d6ff">&#34;boolean&#34;</span>);
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>test(<span style="color:#a5d6ff">&#34;測試 hideComplete 函式運作&#34;</span>, <span style="color:#ff7b72">async</span> () =&gt; {
</span></span><span style="display:flex;"><span>  mockedAxios.get.mockResolvedValue(data);
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">let</span> result <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> fetchData();
</span></span><span style="display:flex;"><span>  result <span style="color:#ff7b72;font-weight:bold">=</span> hideComplete(result);
</span></span><span style="display:flex;"><span>  expect(result[<span style="color:#a5d6ff">0</span>].completed).toBe(<span style="color:#79c0ff">false</span>);
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><p>輸入<code>npm run test -t path/user.test.ts</code>後，如果順利就會看到下圖的測試結果嚕！</p>
<p><img src="https://deanlin.net/blog/202301/jest-advanced-mock-setup-teardown/images/jest-advanced-mock-setup-teardown-04.webp" width="1256" height="616" alt="三、用 Mock Function 減少測試時的相依性（圖 4）" loading="lazy" decoding="async"></p>
<hr>
<h2 id="四vscode-的-jest-外掛">四、VSCode 的 Jest 外掛<a class="h-anchor" href="#%e5%9b%9bvscode-%e7%9a%84-jest-%e5%a4%96%e6%8e%9b" aria-label="這個段落的連結"></a></h2>
<p>每新增一個測試就要跑一次 <code>npm run test</code> 是不是很煩？如果想獨立測試某個檔案還要先確認文件路徑真的讓人心累 😨</p>
<p>但好在 VSCode 有「<a href="https://marketplace.visualstudio.com/items?itemName=Orta.vscode-jest">Jest</a>」這個 Extensions 讓我們事半功倍。</p>
<p><img src="https://deanlin.net/blog/202301/jest-advanced-mock-setup-teardown/images/jest-advanced-mock-setup-teardown-05.webp" width="1400" height="480" alt="四、VSCode 的 Jest 外掛（圖 5）" loading="lazy" decoding="async"></p>
<p>安裝完成後，VSCode 的左側會多出一個「Testing」的 icon，只要你在專案下有設定 <code>jest.config.js</code> 就會出現（如果還未設定可以參考<a href="https://deanlin.net/blog/202301/jest-unit-testing-typescript/">上一篇文章</a>）。</p>
<p><img src="https://deanlin.net/blog/202301/jest-advanced-mock-setup-teardown/images/jest-advanced-mock-setup-teardown-06.webp" width="1400" height="615" alt="四、VSCode 的 Jest 外掛（圖 6）" loading="lazy" decoding="async"></p>
<p>在 test 文檔左側即可看到測試成功的「v」與失敗的「x」。</p>
<p><img src="https://deanlin.net/blog/202301/jest-advanced-mock-setup-teardown/images/jest-advanced-mock-setup-teardown-07.webp" width="1048" height="462" alt="四、VSCode 的 Jest 外掛（圖 7）" loading="lazy" decoding="async"></p>
<p>失敗的測試會在 expect 下方出現紅色底線，滑鼠移過去就會顯示預期值（Expected）與接收值（Received）的差異。</p>
<p><img src="https://deanlin.net/blog/202301/jest-advanced-mock-setup-teardown/images/jest-advanced-mock-setup-teardown-08.webp" width="1400" height="641" alt="四、VSCode 的 Jest 外掛（圖 8）" loading="lazy" decoding="async"></p>
<blockquote>
<p><strong>備註</strong>：身邊有朋友在安裝外掛後遇到了一些奇怪的 Error 無法正常使用，重開 VSCode、看官方文檔怎麼調整都無用，最後用大絕「重新開機」就正常了</p>
</blockquote>
<hr>
<p>常用的 Jest 應用就介紹到這邊，真的要自己動手實驗過才會理解裏面的原理喔！</p>
<a class="linkcard" href="https://deanlin.net/series/frontend-testing/">
  <span class="lc-body">
    <span class="lc-title">前端單元測試</span>
    <span class="lc-desc">替前端專案導入測試：學習地圖、Jest 基礎與進階，再到 Vuetify（Vite &amp; TypeScript）中的 @vue/test-utils。</span>
    <span class="lc-site"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M5 12h14M13 6l6 6-6 6"/></svg>林鼎淵 Dean Lin</span>
  </span>
</a>

]]></content:encoded>
    </item>
    <item>
      <title>認識 Jest 測試框架，開始動手寫單元測試（TypeScript）</title>
      <link>https://deanlin.net/blog/202301/jest-unit-testing-typescript/</link>
      <pubDate>Thu, 19 Jan 2023 10:55:37 +0800</pubDate>
      <guid>https://deanlin.net/blog/202301/jest-unit-testing-typescript/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>這篇文章會從建立專案開始，依序帶你了解需要設定的環境、單元測試的結構，接著帶你完成第一個單元測試。 在對單元測試有基礎的概念後，會透過實際的範例讓你了解不同匹配器（Matchers）的使用時機，話不多說，我們直接開始吧！</description>
      <media:content url="https://deanlin.net/blog/202301/jest-unit-testing-typescript/images/jest-unit-testing-typescript-og_hu_b8982ad14b0c0dbf.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<blockquote>
<p>如果你還在猶豫是否學習測試框架，不妨先看看筆者在<a href="https://deanlin.net/blog/202301/frontend-testing-roadmap/">上一篇文章</a>的分享喔！</p>
</blockquote>
<p>這篇文章會從建立專案開始，依序帶你了解需要設定的環境、單元測試的結構，接著帶你完成第一個單元測試。</p>
<p>在對單元測試有基礎的概念後，會透過實際的範例讓你了解不同匹配器（Matchers）的使用時機，話不多說，我們直接開始吧！</p>
<p><img src="https://deanlin.net/blog/202301/jest-unit-testing-typescript/images/jest-unit-testing-typescript-01.webp" width="1147" height="599" alt="認識 Jest 測試框架，開始動手寫單元測試（TypeScript）（圖 1）" fetchpriority="high" decoding="async"></p>
<pre tabindex="0"><code>大綱

一、初始化專案、設定測試環境
二、了解單元測試的結構，撰寫程式
三、了解 Jest 常用的匹配器（Matchers）
</code></pre><blockquote>
<p>讀者可以直接到<a href="https://github.com/dean9703111/jest-typescript-starter/tree/master/jest-basic">筆者的 Github</a> ，將專案 Clone 下來跑跑看，會更好理解裏面的邏輯（順手按個 ⭐ 更好 🤩）。</p>
</blockquote>
<h2 id="一初始化專案設定測試環境">一、初始化專案、設定測試環境<a class="h-anchor" href="#%e4%b8%80%e5%88%9d%e5%a7%8b%e5%8c%96%e5%b0%88%e6%a1%88%e8%a8%ad%e5%ae%9a%e6%b8%ac%e8%a9%a6%e7%92%b0%e5%a2%83" aria-label="這個段落的連結"></a></h2>
<p><strong>SETP 1</strong>：初始化專案＆安裝 Jest。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>mkdir jest-practice
</span></span><span style="display:flex;"><span>cd jest-practice
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>npm init -f
</span></span><span style="display:flex;"><span>npm install jest babel-jest ts-jest @types/jest
</span></span></code></pre></div><p><strong>SETP 2</strong>：進入「package.json」增加測試用 scripts。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f85149">...</span>
</span></span><span style="display:flex;"><span>  <span style="color:#7ee787">&#34;scripts&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#7ee787">&#34;test&#34;</span>: <span style="color:#a5d6ff">&#34;jest&#34;</span>
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>  <span style="color:#f85149">...</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>SETP 3</strong>：在根目錄下新增「jest.config.ts」。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span>module.exports <span style="color:#ff7b72;font-weight:bold">=</span> {
</span></span><span style="display:flex;"><span>  transform<span style="color:#ff7b72;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a5d6ff">&#39;^.+\\.tsx?$&#39;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;ts-jest&#39;</span>,
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>  moduleFileExtensions<span style="color:#ff7b72;font-weight:bold">:</span> [<span style="color:#a5d6ff">&#39;ts&#39;</span>],
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><p><strong>SETP 4</strong>：在根目錄下新增「tsconfig.json」。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#7ee787">&#34;compilerOptions&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#7ee787">&#34;esModuleInterop&#34;</span>: <span style="color:#79c0ff">true</span>,
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h2 id="二了解單元測試的結構撰寫程式">二、了解單元測試的結構，撰寫程式<a class="h-anchor" href="#%e4%ba%8c%e4%ba%86%e8%a7%a3%e5%96%ae%e5%85%83%e6%b8%ac%e8%a9%a6%e7%9a%84%e7%b5%90%e6%a7%8b%e6%92%b0%e5%af%ab%e7%a8%8b%e5%bc%8f" aria-label="這個段落的連結"></a></h2>
<p>我們就用最簡單的「加法」來做說明：<code>a + b = c</code></p>
<p>如果把加法寫成單元測試，可以拆解成下面的結構：</p>
<ul>
<li><strong>測試的目標</strong>：我們要證明 <code>a + b = c</code>。</li>
<li><strong>要測試的 function</strong>：要測試 <code>sum</code> 這個 function。</li>
<li><strong>測試期待的結果</strong>：確認 sum 執行 <code>a + b</code> 的結果等於 <code>c</code>。</li>
</ul>
<p><strong>SETP 1</strong>：在專案下新增兩個檔案</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># 放測試的 function</span>
</span></span><span style="display:flex;"><span>touch public.ts
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># 測試的腳本</span>
</span></span><span style="display:flex;"><span>touch public.test.ts
</span></span></code></pre></div><p><strong>SETP 2</strong>：在 public.ts 撰寫 <code>sum</code> 這個 function，並於 public.test.ts 用測試程式來驗證他。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// public.ts
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">export</span> <span style="color:#ff7b72">function</span> sum (a: <span style="color:#ff7b72">number</span> ,b: <span style="color:#ff7b72">number</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">return</span> a <span style="color:#ff7b72;font-weight:bold">+</span> b;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// public.test.ts
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { sum } <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">require</span>(<span style="color:#a5d6ff">&#39;./public&#39;</span>);
</span></span><span style="display:flex;"><span>test(<span style="color:#a5d6ff">&#39;測試 8 + 9 等於 17&#39;</span>, () <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>    expect(sum(<span style="color:#a5d6ff">8</span>, <span style="color:#a5d6ff">9</span>)).toBe(<span style="color:#a5d6ff">17</span>);
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><p><strong>SETP 3</strong>：運行測試指令 <code>npm run test</code> ，如果順利就會看到下圖的結果：</p>
<p><img src="https://deanlin.net/blog/202301/jest-unit-testing-typescript/images/jest-unit-testing-typescript-02.webp" width="694" height="520" alt="二、了解單元測試的結構，撰寫程式（圖 2）" loading="lazy" decoding="async"></p>
<p>最後我們再來回顧一下先前提到的結構：</p>
<ul>
<li><strong>測試的目標</strong>：test(<code>測試 8 + 9 等於 17</code>, () =&gt; {});</li>
<li><strong>要測試的 function</strong>：<code>sum(8, 9)</code></li>
<li><strong>測試期待的結果</strong>：expect(sum(8, 9)).toBe(<code>17</code>);</li>
</ul>
<hr>
<h2 id="三了解-jest-常用的匹配器matchers">三、了解 Jest 常用的匹配器（Matchers）<a class="h-anchor" href="#%e4%b8%89%e4%ba%86%e8%a7%a3-jest-%e5%b8%b8%e7%94%a8%e7%9a%84%e5%8c%b9%e9%85%8d%e5%99%a8matchers" aria-label="這個段落的連結"></a></h2>
<p>匹配器（Matchers）是讓我們驗證執行結果是否符合預期的工具，下面筆者放上幾個常用的範例。</p>
<ul>
<li>toBe：最常用的匹配器。</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span>test(<span style="color:#a5d6ff">&#39;toBe 匹配器其實能取代許多 Matcher&#39;</span>, () <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>    expect(<span style="color:#a5d6ff">1</span> <span style="color:#ff7b72;font-weight:bold">+</span> <span style="color:#a5d6ff">2</span>).toBe(<span style="color:#a5d6ff">3</span>);
</span></span><span style="display:flex;"><span>    expect(<span style="color:#79c0ff">null</span>).toBe(<span style="color:#79c0ff">null</span>);
</span></span><span style="display:flex;"><span>    expect(<span style="color:#79c0ff">null</span>).toBeNull();
</span></span><span style="display:flex;"><span>    expect(<span style="color:#79c0ff">undefined</span>).toBe(<span style="color:#79c0ff">undefined</span>);
</span></span><span style="display:flex;"><span>    expect(<span style="color:#79c0ff">undefined</span>).toBeUndefined();
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><ul>
<li>toEqual：如果要物件（object）間的比對可以使用它。</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span>test(<span style="color:#a5d6ff">&#39;toEqual 主要是比對物件&#39;</span>, () <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> data <span style="color:#ff7b72;font-weight:bold">=</span> { one: <span style="color:#ff7b72">1</span> };
</span></span><span style="display:flex;"><span>    data[<span style="color:#a5d6ff">&#39;two&#39;</span>] <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">2</span>;
</span></span><span style="display:flex;"><span>    expect(data).toEqual({ one: <span style="color:#ff7b72">1</span>, two: <span style="color:#ff7b72">2</span> });
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><ul>
<li>toBeCloseTo：應用在小數點的計算。</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span>test(<span style="color:#a5d6ff">&#39;小數點相加&#39;</span>, () <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> value <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">0.1</span> <span style="color:#ff7b72;font-weight:bold">+</span> <span style="color:#a5d6ff">0.2</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 有小數點的請使用 toBeCloseTo，否則會有 rounding error
</span></span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// expect(value).toBe(0.3);
</span></span></span><span style="display:flex;"><span>    expect(value).toBeCloseTo(<span style="color:#a5d6ff">0.3</span>); <span style="color:#8b949e;font-style:italic">// This works.
</span></span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><ul>
<li>toMatch：字串比對。</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span>test(<span style="color:#a5d6ff">&#39;透過 toMatch 確認「stop」是否存在於字串&#39;</span>, () =&gt; {
</span></span><span style="display:flex;"><span>    expect(<span style="color:#a5d6ff">&#39;Christoph&#39;</span>).toMatch(<span style="color:#79c0ff">/stop/</span>);
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><ul>
<li>toContain：確認「值」是否存在於陣列。</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span>test(<span style="color:#a5d6ff">&#39;toContain 會透過迭代來確認陣列是否存在某值&#39;</span>, () =&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> shoppingList <span style="color:#ff7b72;font-weight:bold">=</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#a5d6ff">&#39;diapers&#39;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a5d6ff">&#39;kleenex&#39;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a5d6ff">&#39;trash bags&#39;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a5d6ff">&#39;paper towels&#39;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a5d6ff">&#39;milk&#39;</span>,
</span></span><span style="display:flex;"><span>    ];
</span></span><span style="display:flex;"><span>    expect(shoppingList).toContain(<span style="color:#a5d6ff">&#39;milk&#39;</span>);
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><ul>
<li>toThrow：確認吐出的錯誤訊息是否符合期待。</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span>test(<span style="color:#a5d6ff">&#39;toThrow 可以用來檢核錯誤訊息&#39;</span>, () =&gt; {
</span></span><span style="display:flex;"><span>    expect(() =&gt; doSomething()).toThrow();
</span></span><span style="display:flex;"><span>    expect(() =&gt; doSomething()).toThrow(Error);
</span></span><span style="display:flex;"><span>    expect(() =&gt; doSomething()).toThrow(<span style="color:#a5d6ff">&#39;Something wrong&#39;</span>);
</span></span><span style="display:flex;"><span>    expect(() =&gt; doSomething()).toThrow(<span style="color:#79c0ff">/wrong/</span>);
</span></span><span style="display:flex;"><span>    expect(() =&gt; doSomething()).toThrow(<span style="color:#79c0ff">/^Something wrong!$/</span>);<span style="color:#8b949e;font-style:italic">// 若要求完全相同
</span></span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><p>以上是 Jest 測試框架的入門應用，如果想更深入的了解，可以參考筆者的文章： <a href="https://deanlin.net/blog/202301/jest-advanced-mock-setup-teardown/">Jest 框架進階應用（describe、Setup and Teardown、Mock function）</a></p>
<a class="linkcard" href="https://deanlin.net/series/frontend-testing/">
  <span class="lc-body">
    <span class="lc-title">前端單元測試</span>
    <span class="lc-desc">替前端專案導入測試：學習地圖、Jest 基礎與進階，再到 Vuetify（Vite &amp; TypeScript）中的 @vue/test-utils。</span>
    <span class="lc-site"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M5 12h14M13 6l6 6-6 6"/></svg>林鼎淵 Dean Lin</span>
  </span>
</a>

]]></content:encoded>
    </item>
    <item>
      <title>想幫前端專案導入測試框架？也許我的想法與學習地圖能給你一點方向</title>
      <link>https://deanlin.net/blog/202301/frontend-testing-roadmap/</link>
      <pubDate>Thu, 19 Jan 2023 10:06:38 +0800</pubDate>
      <guid>https://deanlin.net/blog/202301/frontend-testing-roadmap/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>筆者自首，過去我只撰寫後端測試，總是忽略前端測試的重要性。 當初會這樣，是因為經手的專案規格變動頻繁，感覺測試寫完沒多久就要棄用；但近期與業界的朋友聊天時，他們跟我強調了 TDD（Test-Driven Development），也就是「測試驅動開發」的概念。</description>
      <media:content url="https://deanlin.net/blog/202301/frontend-testing-roadmap/images/frontend-testing-roadmap-og_hu_e01158bc3b725f5a.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<p><img src="https://deanlin.net/blog/202301/frontend-testing-roadmap/images/frontend-testing-roadmap-01.webp" width="1150" height="602" alt="想幫前端專案導入測試框架？也許我的想法與學習地圖能給你一點方向（圖 1）" fetchpriority="high" decoding="async"></p>
<pre tabindex="0"><code>大綱

一、前端為什麼需要寫測試？
二、寫測試對開發人員有哪些幫助？
三、筆者的學習地圖
</code></pre><h2 id="一前端為什麼需要寫測試">一、前端為什麼需要寫測試？<a class="h-anchor" href="#%e4%b8%80%e5%89%8d%e7%ab%af%e7%82%ba%e4%bb%80%e9%ba%bc%e9%9c%80%e8%a6%81%e5%af%ab%e6%b8%ac%e8%a9%a6" aria-label="這個段落的連結"></a></h2>
<p>筆者自首，過去我只撰寫後端測試，總是忽略前端測試的重要性。</p>
<p>當初會這樣，是因為經手的專案規格變動頻繁，感覺測試寫完沒多久就要棄用；但近期與業界的朋友聊天時，他們跟我強調了 TDD（Test-Driven Development），也就是「測試驅動開發」的概念。</p>
<p>這名詞也不是第一次聽到，但是否對專案有幫助我也是心存懷疑。</p>
<p>針對這個疑問，朋友很認真的回應我，這邊整理概要如下：</p>
<blockquote>
<p>規格之所以會頻繁變更，大多數的原因是前期規劃不良所導致。</p>
<p>有些 PM 甚至沒有技術背景只能當傳聲筒，在這種前提下訂出的規格往往非常模糊。</p>
<p>而工程師面對這種模糊的規格，他有兩條路，一條路是不管那麼多，先開發再說，如果選擇這條路，那日後頻繁的遇到規格變更實屬正常。</p>
<p>而另一條路則是「確定規格後」再開發，我們常常把沒寫測試歸咎於專案時程太緊；但其實在專案完成後做 Review 時，發現很多時候都是在白忙一場。</p>
<p>所以如果能先把模糊的地方定義清楚，並將其納入測試程式，有時反而是在節省時間，要是日後規格有異動時，也方便確認哪一塊需要調整。</p>
</blockquote>
<p>每個人學習新技術的理由不盡相同，但我就是在這次聊天後開始學習的。</p>
<blockquote class="pullquote"><p><em>其實有時候也不是沒時間，只是因為自己不熟悉測試程式，而用其他藉口逃避。</em></p>
</blockquote>
<hr>
<h2 id="二寫測試對開發人員有哪些幫助">二、寫測試對開發人員有哪些幫助？<a class="h-anchor" href="#%e4%ba%8c%e5%af%ab%e6%b8%ac%e8%a9%a6%e5%b0%8d%e9%96%8b%e7%99%bc%e4%ba%ba%e5%93%a1%e6%9c%89%e5%93%aa%e4%ba%9b%e5%b9%ab%e5%8a%a9" aria-label="這個段落的連結"></a></h2>
<p>系統會越來越複雜，公司的同事會來來去去，不管是為了自己還是同事，有測試程式對我們最直接的幫助有：</p>
<ul>
<li><strong>減少人工測試</strong>：如果每次 release 新的版本就要全部手動測試一次，你絕對會崩潰。</li>
<li><strong>減少測試上的遺漏</strong>：如果是人工測試，那就可能有「啊！這功能之前都沒問題，這次就不用測了」的想法，然後上線後就爆了；不過交給機器跑就不會有這個問題。</li>
<li><strong>避免調整程式後，一堆功能壞掉而不自知</strong>：無論是重構程式/新增功能/解決 Bug，你勢必都會動到程式邏輯，很有可能你完成了新功能，但上線後才發現其他相依的功能壞掉了（程式也是有蝴蝶效應的）。</li>
<li><strong>未來的同事（自己）會感謝你</strong>：專案可能上線後一兩年都沒事，但當需要新增/調整功能時，如果維護的人發現前人沒有測試程式，那改程式就跟走鋼索一樣（那個人很可能是自己）。</li>
</ul>
<hr>
<h2 id="三筆者的學習地圖">三、筆者的學習地圖<a class="h-anchor" href="#%e4%b8%89%e7%ad%86%e8%80%85%e7%9a%84%e5%ad%b8%e7%bf%92%e5%9c%b0%e5%9c%96" aria-label="這個段落的連結"></a></h2>
<p>把知識集中在一篇文章會讀得太辛苦，這邊筆者將其規劃成以下主題供大家閱讀。</p>
<h3 id="-認識-jest-測試框架開始動手寫測試程式link">認識 Jest 測試框架，開始動手寫測試程式：<a href="https://deanlin.net/blog/202301/jest-unit-testing-typescript/">link</a><a class="h-anchor" href="#-%e8%aa%8d%e8%ad%98-jest-%e6%b8%ac%e8%a9%a6%e6%a1%86%e6%9e%b6%e9%96%8b%e5%a7%8b%e5%8b%95%e6%89%8b%e5%af%ab%e6%b8%ac%e8%a9%a6%e7%a8%8b%e5%bc%8flink" aria-label="這個段落的連結"></a></h3>
<ul>
<li>初始化專案、設定測試環境</li>
<li>了解單元測試的結構，撰寫程式</li>
<li>了解 Jest 常用的匹配器（Matchers）</li>
</ul>
<h3 id="-jest-框架進階應用describesetup-and-teardownmock-functionlink">Jest 框架進階應用（describe、Setup and Teardown、Mock function）：<a href="https://deanlin.net/blog/202301/jest-advanced-mock-setup-teardown/">link</a><a class="h-anchor" href="#-jest-%e6%a1%86%e6%9e%b6%e9%80%b2%e9%9a%8e%e6%87%89%e7%94%a8describesetup-and-teardownmock-functionlink" aria-label="這個段落的連結"></a></h3>
<ul>
<li>測試分組（describe）</li>
<li>在測試前、後要執行的任務（Setup and Teardown）</li>
<li>用 Mock Function 減少測試時的相依性</li>
<li>VSCode 的 Jest 外掛</li>
</ul>
<h3 id="-在-vuetifyvite--typescript中導入單元測試-unit-testinglink">在 Vuetify（Vite &amp; TypeScript）中導入單元測試 Unit Testing：<a href="https://deanlin.net/blog/202301/vuetify-vite-unit-testing/">link</a><a class="h-anchor" href="#-%e5%9c%a8-vuetifyvite--typescript%e4%b8%ad%e5%b0%8e%e5%85%a5%e5%96%ae%e5%85%83%e6%b8%ac%e8%a9%a6-unit-testinglink" aria-label="這個段落的連結"></a></h3>
<ul>
<li>初始化專案、安裝必要套件</li>
<li>調整專案各項設定檔，讓單元測試可以運行</li>
<li>撰寫第一隻測試程式</li>
<li>運行測試程式確認結果符合預期</li>
</ul>
<h3 id="-透過實際案例了解在-vuetifyvite--typescript中如何運用-vuetest-utilslink">透過實際案例了解在 Vuetify（Vite &amp; TypeScript）中如何運用 @vue/test-utils：<a href="https://deanlin.net/blog/202301/vue-test-utils-examples/">link</a><a class="h-anchor" href="#-%e9%80%8f%e9%81%8e%e5%af%a6%e9%9a%9b%e6%a1%88%e4%be%8b%e4%ba%86%e8%a7%a3%e5%9c%a8-vuetifyvite--typescript%e4%b8%ad%e5%a6%82%e4%bd%95%e9%81%8b%e7%94%a8-vuetest-utilslink" aria-label="這個段落的連結"></a></h3>
<ul>
<li>從測試的角度來設計頁面規格（以登入頁為範例）</li>
<li>撰寫登入頁程式</li>
<li>從測試案例了解 @vue/test-utils 常用的功能（ex：getComponent、setValue、wrapper.vm、trigger button、Vue Router）</li>
</ul>
]]></content:encoded>
    </item>
  </channel>
</rss>
