<?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 Testing｜林鼎淵 Dean Lin</title>
    <link>https://deanlin.net/tags/unit-testing/</link>
    <image><url>https://deanlin.net/images/author_hu_aa256fc934e9a932.jpg</url><title>Unit Testing｜林鼎淵 Dean Lin</title><link>https://deanlin.net/tags/unit-testing/</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:02 +0800</lastBuildDate>
    <atom:link href="https://deanlin.net/tags/unit-testing/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>透過實際案例了解在 Vuetify（Vite &amp; TypeScript）中如何運用 @vue/test-utils</title>
      <link>https://deanlin.net/blog/202301/vue-test-utils-examples/</link>
      <pubDate>Thu, 19 Jan 2023 13:58:02 +0800</pubDate>
      <guid>https://deanlin.net/blog/202301/vue-test-utils-examples/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>今天這篇文章，筆者會用一個簡易的「登入頁面」，來介紹 @vue/test-utils 這款套件的常用功能（ex：getComponent、setValue、wrapper.vm、trigger button、Vue Router）</description>
      <media:content url="https://deanlin.net/blog/202301/vue-test-utils-examples/images/vue-test-utils-examples-og_hu_be420e200f2d4408.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<p><a href="https://deanlin.net/blog/202301/vuetify-vite-unit-testing/">上一篇文章中</a>，我們成功在 Vuetify（Vite &amp; TypeScript）中導入單元測試 Unit Testing。</p>
<p>今天這篇文章，筆者會用一個簡易的「登入頁面」，來介紹 @vue/test-utils 這款套件的常用功能：</p>
<ul>
<li>用 <strong>get</strong> 找出元素（element）</li>
<li>用 <strong>getComponent</strong> 找出元件（component）</li>
<li>用 <strong>setValue</strong> 模擬使用者對元件（component）輸入值</li>
<li>透過 <strong>wrapper.vm</strong> 取得 data 資訊</li>
<li>取得元素（element）的 <strong>attributes</strong> 屬性</li>
<li>模擬使用者點擊輸入框（<strong>focus</strong>）輸入文字，然後離開（<strong>blur</strong>）的動作</li>
<li>當操作錯誤時，能捕捉預期的 <strong>error message</strong></li>
<li>確認使用者操作系統時，元件的 <strong>attributes</strong> 如預期改變（ex： disabled、type）</li>
<li>當元素採用 v-show 策略時，用 <strong>isVisible</strong> 進行判斷</li>
<li>模擬使用者 <strong>trigger button</strong> 事件</li>
<li>使用 <strong>Vue Router</strong> 時，頁面是否如預期跳轉</li>
</ul>
<p><img src="https://deanlin.net/blog/202301/vue-test-utils-examples/images/vue-test-utils-examples-01.webp" width="1148" height="599" alt="透過實際案例了解在 Vuetify（Vite &amp; TypeScript）中如何運用 @vue/test-utils（圖 1）" fetchpriority="high" decoding="async"></p>
<pre tabindex="0"><code>大綱

ㄧ、從測試的角度來設計頁面規格
二、撰寫登入頁程式
三、從測試案例了解 @vue/test-utils 常用的功能
  ▶︎ 測試檔案初始設定
  ▶︎ 用 get 找出元素（element），並確認是有顯示在畫面上的（isVisible）
  ▶︎ 透過 getComponent 取出元件，並指定「input」來 setValue
  ▶︎ 在輸入框輸入後，用 wrapper.vm.xxx 確定 data 的有成功寫入
  ▶︎ 取得元素的 attributes，確認設定符合預期
  ▶︎ 模擬使用者點擊輸入框（focus）輸入文字，然後離開（blur）的動作
  ▶︎ 使用者操作系統時，元件的 attributes 如預期改變
  ▶︎ 模擬使用者 trigger button 事件，觀察頁面是否如預期跳轉（Vue Router）
</code></pre><blockquote>
<p><em>讀者可以直接到<a href="https://github.com/dean9703111/-vuetify-vite-ts-test-exmaples">筆者的 Github</a>，將專案 Clone 下來跑跑看，會更好理解裏面的邏輯（順手按個 ⭐ 更好 🤩）。</em></p>
</blockquote>
<h2 id="ㄧ從測試的角度來設計頁面規格">ㄧ、從測試的角度來設計頁面規格<a class="h-anchor" href="#%e3%84%a7%e5%be%9e%e6%b8%ac%e8%a9%a6%e7%9a%84%e8%a7%92%e5%ba%a6%e4%be%86%e8%a8%ad%e8%a8%88%e9%a0%81%e9%9d%a2%e8%a6%8f%e6%a0%bc" aria-label="這個段落的連結"></a></h2>
<p>在<a href="https://deanlin.net/blog/202301/frontend-testing-roadmap/">第一篇文章</a>有談到 TDD（Test-Driven Development），測試驅動開發。</p>
<p>假使我們要設計一個登入頁，可以訂製如下規格：</p>
<h3 id="-基礎元件功能">基礎元件功能<a class="h-anchor" href="#-%e5%9f%ba%e7%a4%8e%e5%85%83%e4%bb%b6%e5%8a%9f%e8%83%bd" aria-label="這個段落的連結"></a></h3>
<ul>
<li>確認 email、password、LoginBtn 元素存在</li>
<li>確認 Email 輸入框可以填資料</li>
<li>確認 Password 輸入框可以填資料</li>
<li>Email、Password 輸入後，data 對應的 formData 有存入相應的值</li>
<li>預設 Password 輸入框的 type 為 password，不直接顯示</li>
</ul>
<h3 id="-測試錯誤的-emailpassword-情境">測試錯誤的 Email、Password 情境<a class="h-anchor" href="#-%e6%b8%ac%e8%a9%a6%e9%8c%af%e8%aa%a4%e7%9a%84-emailpassword-%e6%83%85%e5%a2%83" aria-label="這個段落的連結"></a></h3>
<ul>
<li>當 Email 沒有填寫時，跳出 Required 的錯誤訊息</li>
<li>當 Email 格式錯誤時，跳出相關錯誤訊息</li>
<li>當 Password 沒有填寫時，跳出 Required 的錯誤訊息</li>
<li>當 Password 字數不滿 8 位時，跳出相關錯誤訊息</li>
</ul>
<h3 id="-會讓元件屬性改變的行為">會讓元件屬性改變的行為<a class="h-anchor" href="#-%e6%9c%83%e8%ae%93%e5%85%83%e4%bb%b6%e5%b1%ac%e6%80%a7%e6%94%b9%e8%ae%8a%e7%9a%84%e8%a1%8c%e7%82%ba" aria-label="這個段落的連結"></a></h3>
<ul>
<li>當 Email、Password 未填寫時，登入按鈕為 Disabled</li>
<li>當 Email、Password 格式錯誤時，登入按鈕為 Disabled</li>
<li>當 Email、Password 格式正確時，登入按鈕可點擊</li>
<li>點擊顯示 Password 的 icon 時，type 會轉換成 text</li>
</ul>
<h3 id="-登入功能">登入功能<a class="h-anchor" href="#-%e7%99%bb%e5%85%a5%e5%8a%9f%e8%83%bd" aria-label="這個段落的連結"></a></h3>
<ul>
<li>輸入錯誤的帳號密碼，會彈出警視窗</li>
<li>確認 Router 可以順利導向</li>
</ul>
<blockquote>
<p>上面訂的規格，就是我們後續要測試的情境。</p>
</blockquote>
<hr>
<h2 id="二撰寫登入頁程式">二、撰寫登入頁程式<a class="h-anchor" href="#%e4%ba%8c%e6%92%b0%e5%af%ab%e7%99%bb%e5%85%a5%e9%a0%81%e7%a8%8b%e5%bc%8f" aria-label="這個段落的連結"></a></h2>
<p>在 views 資料夾下新增「Login.vue」來做登入頁，幾個重點說一下：</p>
<ul>
<li>使用 dataset（ex：data-test=”xxx”）來做標記，因為 id 或 class 通常都是因為其他用途而存在的，要是 coding 中不小心動到 id 或是 class 會造成測試不通過</li>
<li>在 v-text-field 有設計「rules」來做校驗</li>
<li>「valid」影響輸入按鈕是否能按，「pwshow」是密碼是否顯示，「loginFail」決定登入失敗的錯誤訊息是否顯示</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-html" data-lang="html"><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">div</span> class<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;d-flex align-center flex-column&#34;</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#7ee787">v-card</span> width<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;400&#34;</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#7ee787">v-container</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#7ee787">v-form</span> v-model<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;valid&#34;</span> lazy-validation&gt;
</span></span><span style="display:flex;"><span>          &lt;<span style="color:#7ee787">v-alert</span>
</span></span><span style="display:flex;"><span>            v-show<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;loginFail&#34;</span>
</span></span><span style="display:flex;"><span>            type<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;error&#34;</span>
</span></span><span style="display:flex;"><span>            class<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;mb-4&#34;</span>
</span></span><span style="display:flex;"><span>            data-test<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;loginAlert&#34;</span>
</span></span><span style="display:flex;"><span>          &gt;
</span></span><span style="display:flex;"><span>            You are entering an incorrect email and/or password.
</span></span><span style="display:flex;"><span>          &lt;/<span style="color:#7ee787">v-alert</span>&gt;
</span></span><span style="display:flex;"><span>          &lt;<span style="color:#7ee787">v-text-field</span>
</span></span><span style="display:flex;"><span>            v-model<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;formData.email&#34;</span>
</span></span><span style="display:flex;"><span>            :rules<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;emailRules&#34;</span>
</span></span><span style="display:flex;"><span>            label<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;Email&#34;</span>
</span></span><span style="display:flex;"><span>            required
</span></span><span style="display:flex;"><span>            data-test<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;email&#34;</span>
</span></span><span style="display:flex;"><span>          &gt;&lt;/<span style="color:#7ee787">v-text-field</span>&gt;
</span></span><span style="display:flex;"><span>          &lt;<span style="color:#7ee787">v-text-field</span>
</span></span><span style="display:flex;"><span>            v-model<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;formData.password&#34;</span>
</span></span><span style="display:flex;"><span>            :append-icon<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;pwShow ? &#39;mdi-eye&#39; : &#39;mdi-eye-off&#39;&#34;</span>
</span></span><span style="display:flex;"><span>            :rules<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;[rules.required, rules.min]&#34;</span>
</span></span><span style="display:flex;"><span>            :type<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;pwShow ? &#39;text&#39; : &#39;password&#39;&#34;</span>
</span></span><span style="display:flex;"><span>            name<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;input-10-1&#34;</span>
</span></span><span style="display:flex;"><span>            label<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;Password&#34;</span>
</span></span><span style="display:flex;"><span>            hint<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;At least 8 characters&#34;</span>
</span></span><span style="display:flex;"><span>            counter
</span></span><span style="display:flex;"><span>            <span style="color:#f85149">@</span>click:append<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;pwShow = !pwShow&#34;</span>
</span></span><span style="display:flex;"><span>            data-test<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;password&#34;</span>
</span></span><span style="display:flex;"><span>          &gt;&lt;/<span style="color:#7ee787">v-text-field</span>&gt;
</span></span><span style="display:flex;"><span>          &lt;<span style="color:#7ee787">v-spacer</span>&gt;&lt;/<span style="color:#7ee787">v-spacer</span>&gt;
</span></span><span style="display:flex;"><span>          &lt;<span style="color:#7ee787">v-btn</span>
</span></span><span style="display:flex;"><span>            x-large
</span></span><span style="display:flex;"><span>            block
</span></span><span style="display:flex;"><span>            :disabled<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;!valid&#34;</span>
</span></span><span style="display:flex;"><span>            color<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;success&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f85149">@</span>click<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;login&#34;</span>
</span></span><span style="display:flex;"><span>            data-test<span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#a5d6ff">&#34;loginBtn&#34;</span>
</span></span><span style="display:flex;"><span>          &gt;
</span></span><span style="display:flex;"><span>            Login
</span></span><span style="display:flex;"><span>          &lt;/<span style="color:#7ee787">v-btn</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;/<span style="color:#7ee787">v-form</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;/<span style="color:#7ee787">v-container</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#7ee787">v-card</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;/<span style="color:#7ee787">div</span>&gt;
</span></span><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">script</span> setup&gt;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">import</span> { ref, reactive } from <span style="color:#a5d6ff">&#34;vue&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">import</span> router from <span style="color:#a5d6ff">&#34;@/router&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> valid <span style="color:#ff7b72;font-weight:bold">=</span> ref(<span style="color:#79c0ff">true</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> pwShow <span style="color:#ff7b72;font-weight:bold">=</span> ref(<span style="color:#79c0ff">false</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> loginFail <span style="color:#ff7b72;font-weight:bold">=</span> ref(<span style="color:#79c0ff">false</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> formData <span style="color:#ff7b72;font-weight:bold">=</span> reactive({
</span></span><span style="display:flex;"><span>  email<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;&#34;</span>,
</span></span><span style="display:flex;"><span>  password<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;&#34;</span>,
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> emailRules <span style="color:#ff7b72;font-weight:bold">=</span> [
</span></span><span style="display:flex;"><span>  (v) =&gt; <span style="color:#ff7b72;font-weight:bold">!!</span>v <span style="color:#ff7b72;font-weight:bold">||</span> <span style="color:#a5d6ff">&#34;Required&#34;</span>,
</span></span><span style="display:flex;"><span>  (v) =&gt; <span style="color:#ff7b72;font-weight:bold">/</span>.<span style="color:#ff7b72;font-weight:bold">+</span><span style="color:#f85149">@</span>.<span style="color:#ff7b72;font-weight:bold">+</span><span style="color:#f85149">\</span>..<span style="color:#ff7b72;font-weight:bold">+</span><span style="color:#f85149">/.test(v) || &#34;E-mail must be valid&#34;,</span>
</span></span><span style="display:flex;"><span>];
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> rules <span style="color:#ff7b72;font-weight:bold">=</span> {
</span></span><span style="display:flex;"><span>  required<span style="color:#ff7b72;font-weight:bold">:</span> (value) =&gt; <span style="color:#ff7b72;font-weight:bold">!!</span>value <span style="color:#ff7b72;font-weight:bold">||</span> <span style="color:#a5d6ff">&#34;Required&#34;</span>,
</span></span><span style="display:flex;"><span>  min<span style="color:#ff7b72;font-weight:bold">:</span> (v) =&gt; (v <span style="color:#ff7b72;font-weight:bold">&amp;&amp;</span> v.length <span style="color:#ff7b72;font-weight:bold">&gt;=</span> <span style="color:#a5d6ff">8</span>) <span style="color:#ff7b72;font-weight:bold">||</span> <span style="color:#a5d6ff">&#34;Min 8 characters&#34;</span>,
</span></span><span style="display:flex;"><span>};
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> login <span style="color:#ff7b72;font-weight:bold">=</span> () =&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#8b949e;font-style:italic">// 這邊只是為了方便理解，實際上應該要呼叫 api
</span></span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">if</span> (
</span></span><span style="display:flex;"><span>    formData.email <span style="color:#ff7b72;font-weight:bold">===</span> <span style="color:#a5d6ff">&#34;baoboa@mail.com&#34;</span> <span style="color:#ff7b72;font-weight:bold">&amp;&amp;</span>
</span></span><span style="display:flex;"><span>    formData.password <span style="color:#ff7b72;font-weight:bold">===</span> <span style="color:#a5d6ff">&#34;baobaocute&#34;</span>
</span></span><span style="display:flex;"><span>  ) {
</span></span><span style="display:flex;"><span>    router.push(<span style="color:#a5d6ff">&#34;/home&#34;</span>);
</span></span><span style="display:flex;"><span>  } <span style="color:#ff7b72">else</span> {
</span></span><span style="display:flex;"><span>    loginFail.value <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>&lt;/<span style="color:#7ee787">script</span>&gt;
</span></span></code></pre></div><p>完成後在終端機輸入 <code>yarn dev</code>，然後進入網址：<a href="http://localhost:3000/">http://localhost:3000</a></p>
<p><img src="https://deanlin.net/blog/202301/vue-test-utils-examples/images/vue-test-utils-examples-02.webp" width="1400" height="586" alt="二、撰寫登入頁程式（圖 2）" loading="lazy" decoding="async"></p>
<blockquote>
<p>我這邊有調整「src/router/index.ts」，將 Login 的 path 設定為「/」，讀者可以<a href="https://github.com/dean9703111/vuetify-vite-ts-test-exmaples/blob/master/src/router/index.ts">參考 Github</a>。</p>
</blockquote>
<hr>
<h2 id="三從測試案例了解-vuetest-utils-常用的功能">三、從測試案例了解 @vue/test-utils 常用的功能<a class="h-anchor" href="#%e4%b8%89%e5%be%9e%e6%b8%ac%e8%a9%a6%e6%a1%88%e4%be%8b%e4%ba%86%e8%a7%a3-vuetest-utils-%e5%b8%b8%e7%94%a8%e7%9a%84%e5%8a%9f%e8%83%bd" aria-label="這個段落的連結"></a></h2>
<p>這邊會取幾個測試案例來做解說，完成的範例請參考<a href="https://github.com/dean9703111/-vuetify-vite-ts-test-exmaples">筆者的 Github</a>（順手點個 Star 會讓筆者會開心一整天）。</p>
<h3 id="-測試檔案初始設定">測試檔案初始設定<a class="h-anchor" href="#-%e6%b8%ac%e8%a9%a6%e6%aa%94%e6%a1%88%e5%88%9d%e5%a7%8b%e8%a8%ad%e5%ae%9a" aria-label="這個段落的連結"></a></h3>
<p>我們在 views 資料夾下新增「test」資料夾，並新增「Login.spec.ts」的測試檔案：</p>
<ul>
<li>透過 import 引入我們會使用到的方法</li>
<li>這邊透過 beforeEach 來初始化我們要測試 Login.vue</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> { describe, it, expect, beforeEach, vi } from <span style="color:#a5d6ff">&#34;vitest&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">import</span> { mount, VueWrapper } from <span style="color:#a5d6ff">&#34;@vue/test-utils&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">import</span> Login from <span style="color:#a5d6ff">&#34;../Login.vue&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">import</span> vuetify from <span style="color:#a5d6ff">&#34;@/plugins/vuetify&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">import</span> router from <span style="color:#a5d6ff">&#34;@/router&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">let</span> wrapper<span style="color:#ff7b72;font-weight:bold">:</span> VueWrapper<span style="color:#ff7b72;font-weight:bold">&lt;</span>any<span style="color:#ff7b72;font-weight:bold">&gt;</span>;
</span></span><span style="display:flex;"><span>beforeEach(<span style="color:#ff7b72">async</span> () =&gt; {
</span></span><span style="display:flex;"><span>  wrapper <span style="color:#ff7b72;font-weight:bold">=</span> mount(Login, {
</span></span><span style="display:flex;"><span>    global<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, router],
</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><h3 id="-用-get-找出元素element並確認是有顯示在畫面上的isvisible">用 get 找出元素（element），並確認是有顯示在畫面上的（isVisible）<a class="h-anchor" href="#-%e7%94%a8-get-%e6%89%be%e5%87%ba%e5%85%83%e7%b4%a0element%e4%b8%a6%e7%a2%ba%e8%aa%8d%e6%98%af%e6%9c%89%e9%a1%af%e7%a4%ba%e5%9c%a8%e7%95%ab%e9%9d%a2%e4%b8%8a%e7%9a%84isvisible" aria-label="這個段落的連結"></a></h3>
<blockquote>
<p>find 也可以達到一樣的效果，但沒找到時並不會回傳錯誤訊息。</p>
</blockquote>
<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>it(<span style="color:#a5d6ff">&#34;確認 Email、Password、LoginBtn 元素存在&#34;</span>, () <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>  expect(wrapper.<span style="color:#ff7b72">get</span>(<span style="color:#a5d6ff">&#39;[data-test=&#34;email&#34;]&#39;</span>).isVisible()).toBe(<span style="color:#79c0ff">true</span>);
</span></span><span style="display:flex;"><span>  expect(wrapper.<span style="color:#ff7b72">get</span>(<span style="color:#a5d6ff">&#39;[data-test=&#34;password&#34;]&#39;</span>).isVisible()).toBe(<span style="color:#79c0ff">true</span>);
</span></span><span style="display:flex;"><span>  expect(wrapper.<span style="color:#ff7b72">get</span>(<span style="color:#a5d6ff">&#39;[data-test=&#34;loginBtn&#34;]&#39;</span>).isVisible()).toBe(<span style="color:#79c0ff">true</span>);
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><h3 id="-透過-getcomponent-取出元件並指定input來-setvalue">透過 getComponent 取出元件，並指定「input」來 setValue<a class="h-anchor" href="#-%e9%80%8f%e9%81%8e-getcomponent-%e5%8f%96%e5%87%ba%e5%85%83%e4%bb%b6%e4%b8%a6%e6%8c%87%e5%ae%9ainput%e4%be%86-setvalue" aria-label="這個段落的連結"></a></h3>
<blockquote>
<p>因為 Vuetify 的 v-text-field 是 div 層，所以需要再透過 get 往下找到 input 這個元素。</p>
</blockquote>
<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>it(<span style="color:#a5d6ff">&#34;確認 Email 輸入框可以填資料&#34;</span>, <span style="color:#ff7b72">async</span> () <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> emailInput <span style="color:#ff7b72;font-weight:bold">=</span> wrapper.getComponent(<span style="color:#a5d6ff">&#39;[data-test=&#34;email&#34;]&#39;</span>).<span style="color:#ff7b72">get</span>(<span style="color:#a5d6ff">&#34;input&#34;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> emailInput.setValue(<span style="color:#a5d6ff">&#34;test@mail.com&#34;</span>);
</span></span><span style="display:flex;"><span>  expect(emailInput.element.value).toBe(<span style="color:#a5d6ff">&#34;test@mail.com&#34;</span>);
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><h3 id="-在輸入框輸入後用-wrappervmxxx-確定-data-的有成功寫入">在輸入框輸入後，用 wrapper.vm.xxx 確定 data 的有成功寫入<a class="h-anchor" href="#-%e5%9c%a8%e8%bc%b8%e5%85%a5%e6%a1%86%e8%bc%b8%e5%85%a5%e5%be%8c%e7%94%a8-wrappervmxxx-%e7%a2%ba%e5%ae%9a-data-%e7%9a%84%e6%9c%89%e6%88%90%e5%8a%9f%e5%af%ab%e5%85%a5" aria-label="這個段落的連結"></a></h3>
<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>it(<span style="color:#a5d6ff">&#34;Email、Password 輸入後，data 對應的 formData 有寫入相應的值&#34;</span>, <span style="color:#ff7b72">async</span> () <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> formData <span style="color:#ff7b72;font-weight:bold">=</span> {
</span></span><span style="display:flex;"><span>    email<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;test@mail.com&#34;</span>,
</span></span><span style="display:flex;"><span>    password<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;qazwsx1234&#34;</span>,
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> emailInput <span style="color:#ff7b72;font-weight:bold">=</span> wrapper.getComponent(<span style="color:#a5d6ff">&#39;[data-test=&#34;email&#34;]&#39;</span>).<span style="color:#ff7b72">get</span>(<span style="color:#a5d6ff">&#34;input&#34;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> emailInput.setValue(formData.email);
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> passwordInput <span style="color:#ff7b72;font-weight:bold">=</span> wrapper
</span></span><span style="display:flex;"><span>    .getComponent(<span style="color:#a5d6ff">&#39;[data-test=&#34;password&#34;]&#39;</span>)
</span></span><span style="display:flex;"><span>    .<span style="color:#ff7b72">get</span>(<span style="color:#a5d6ff">&#34;input&#34;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> passwordInput.setValue(formData.password);
</span></span><span style="display:flex;"><span>  expect(wrapper.vm.formData).toEqual(formData);
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><h3 id="-取得元素的-attributes確認設定符合預期">取得元素的 attributes，確認設定符合預期<a class="h-anchor" href="#-%e5%8f%96%e5%be%97%e5%85%83%e7%b4%a0%e7%9a%84-attributes%e7%a2%ba%e8%aa%8d%e8%a8%ad%e5%ae%9a%e7%ac%a6%e5%90%88%e9%a0%90%e6%9c%9f" aria-label="這個段落的連結"></a></h3>
<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>it(<span style="color:#a5d6ff">&#34;預設 Password 輸入框的 type 為 password，不直接顯示&#34;</span>, <span style="color:#ff7b72">async</span> () <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> password <span style="color:#ff7b72;font-weight:bold">=</span> wrapper.getComponent(<span style="color:#a5d6ff">&#39;[data-test=&#34;password&#34;]&#39;</span>);
</span></span><span style="display:flex;"><span>  expect(password.<span style="color:#ff7b72">get</span>(<span style="color:#a5d6ff">&#34;input&#34;</span>).attributes().<span style="color:#ff7b72">type</span>).toBe(<span style="color:#a5d6ff">&#34;password&#34;</span>);
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><h3 id="-模擬使用者點擊輸入框focus輸入文字然後離開blur的動作並確認操作錯誤時能捕捉預期的-error-message">模擬使用者點擊輸入框（focus）輸入文字，然後離開（blur）的動作；並確認操作錯誤時，能捕捉預期的 error message<a class="h-anchor" href="#-%e6%a8%a1%e6%93%ac%e4%bd%bf%e7%94%a8%e8%80%85%e9%bb%9e%e6%93%8a%e8%bc%b8%e5%85%a5%e6%a1%86focus%e8%bc%b8%e5%85%a5%e6%96%87%e5%ad%97%e7%84%b6%e5%be%8c%e9%9b%a2%e9%96%8bblur%e7%9a%84%e5%8b%95%e4%bd%9c%e4%b8%a6%e7%a2%ba%e8%aa%8d%e6%93%8d%e4%bd%9c%e9%8c%af%e8%aa%a4%e6%99%82%e8%83%bd%e6%8d%95%e6%8d%89%e9%a0%90%e6%9c%9f%e7%9a%84-error-message" aria-label="這個段落的連結"></a></h3>
<p><img src="https://deanlin.net/blog/202301/vue-test-utils-examples/images/vue-test-utils-examples-03.webp" width="1226" height="766" alt="︎ 模擬使用者點擊輸入框（focus）輸入文字，然後離開（blur）的動作；並確認操作錯誤時，能捕捉預期的 error message（圖 3）" loading="lazy" decoding="async"></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>it(<span style="color:#a5d6ff">&#34;當 Email 沒有填寫時，跳出 Required 的錯誤訊息&#34;</span>, <span style="color:#ff7b72">async</span> () <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> email <span style="color:#ff7b72;font-weight:bold">=</span> wrapper.getComponent(<span style="color:#a5d6ff">&#39;[data-test=&#34;email&#34;]&#39;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> email.trigger(<span style="color:#a5d6ff">&#34;focus&#34;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> email.<span style="color:#ff7b72">get</span>(<span style="color:#a5d6ff">&#34;input&#34;</span>).setValue(<span style="color:#a5d6ff">&#34;123&#34;</span>); <span style="color:#8b949e;font-style:italic">//先隨便塞值
</span></span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> email.<span style="color:#ff7b72">get</span>(<span style="color:#a5d6ff">&#34;input&#34;</span>).setValue(<span style="color:#a5d6ff">&#34;&#34;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> email.trigger(<span style="color:#a5d6ff">&#34;blur&#34;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> emailErrorMsg <span style="color:#ff7b72;font-weight:bold">=</span> email.<span style="color:#ff7b72">get</span>(<span style="color:#a5d6ff">&#34;.v-messages__message&#34;</span>);
</span></span><span style="display:flex;"><span>  expect(emailErrorMsg.text()).toEqual(<span style="color:#a5d6ff">&#34;Required&#34;</span>);
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><h3 id="-使用者操作系統時元件的-attributes-如預期改變exemailpassword-填寫正確登入按鈕-disabled-移除">使用者操作系統時，元件的 attributes 如預期改變（ex：Email、Password 填寫正確，登入按鈕 Disabled 移除）<a class="h-anchor" href="#-%e4%bd%bf%e7%94%a8%e8%80%85%e6%93%8d%e4%bd%9c%e7%b3%bb%e7%b5%b1%e6%99%82%e5%85%83%e4%bb%b6%e7%9a%84-attributes-%e5%a6%82%e9%a0%90%e6%9c%9f%e6%94%b9%e8%ae%8aexemailpassword-%e5%a1%ab%e5%af%ab%e6%ad%a3%e7%a2%ba%e7%99%bb%e5%85%a5%e6%8c%89%e9%88%95-disabled-%e7%a7%bb%e9%99%a4" aria-label="這個段落的連結"></a></h3>
<figure class="figure figure-video">
  <video class="gifv" muted loop playsinline controls preload="none" poster="https://deanlin.net/blog/202301/vue-test-utils-examples/images/vue-test-utils-examples-04.webp" width="626" height="348" aria-label="︎ 使用者操作系統時，元件的 attributes 如預期改變（ex：Email、Password 填寫正確，登入按鈕 Disabled 移除）（圖 4）" data-gifv>
    <source src="https://deanlin.net/blog/202301/vue-test-utils-examples/images/vue-test-utils-examples-04.mp4" type="video/mp4">
  </video>
</figure>

<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>it(<span style="color:#a5d6ff">&#34;當 Email、Password 格式正確時，登入按鈕可點擊&#34;</span>, <span style="color:#ff7b72">async</span> () <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> loginBtn <span style="color:#ff7b72;font-weight:bold">=</span> wrapper.<span style="color:#ff7b72">get</span>(<span style="color:#a5d6ff">&#39;[data-test=&#34;loginBtn&#34;]&#39;</span>);
</span></span><span style="display:flex;"><span>  expect(loginBtn.attributes().disabled).toBeDefined();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> formData <span style="color:#ff7b72;font-weight:bold">=</span> {
</span></span><span style="display:flex;"><span>    email<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;baoboa@mail.com&#34;</span>,
</span></span><span style="display:flex;"><span>    password<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;baobaocute&#34;</span>,
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> email <span style="color:#ff7b72;font-weight:bold">=</span> wrapper.getComponent(<span style="color:#a5d6ff">&#39;[data-test=&#34;email&#34;]&#39;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> email.trigger(<span style="color:#a5d6ff">&#34;focus&#34;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> email.<span style="color:#ff7b72">get</span>(<span style="color:#a5d6ff">&#34;input&#34;</span>).setValue(formData.email);
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> email.trigger(<span style="color:#a5d6ff">&#34;blur&#34;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> password <span style="color:#ff7b72;font-weight:bold">=</span> wrapper.getComponent(<span style="color:#a5d6ff">&#39;[data-test=&#34;password&#34;]&#39;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> password.trigger(<span style="color:#a5d6ff">&#34;focus&#34;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> password.<span style="color:#ff7b72">get</span>(<span style="color:#a5d6ff">&#34;input&#34;</span>).setValue(formData.password);
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> password.trigger(<span style="color:#a5d6ff">&#34;blur&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  expect(loginBtn.attributes().disabled).toBeUndefined();
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><h3 id="-模擬使用者-trigger-button-事件觀察頁面是否如預期跳轉vue-router">模擬使用者 trigger button 事件，觀察頁面是否如預期跳轉（Vue Router）<a class="h-anchor" href="#-%e6%a8%a1%e6%93%ac%e4%bd%bf%e7%94%a8%e8%80%85-trigger-button-%e4%ba%8b%e4%bb%b6%e8%a7%80%e5%af%9f%e9%a0%81%e9%9d%a2%e6%98%af%e5%90%a6%e5%a6%82%e9%a0%90%e6%9c%9f%e8%b7%b3%e8%bd%89vue-router" aria-label="這個段落的連結"></a></h3>
<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>it(<span style="color:#a5d6ff">&#34;確認 Router 可以順利導向&#34;</span>, <span style="color:#ff7b72">async</span> () <span style="color:#ff7b72;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> formData <span style="color:#ff7b72;font-weight:bold">=</span> {
</span></span><span style="display:flex;"><span>    email<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;baoboa@mail.com&#34;</span>,
</span></span><span style="display:flex;"><span>    password<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;baobaocute&#34;</span>,
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> email <span style="color:#ff7b72;font-weight:bold">=</span> wrapper.getComponent(<span style="color:#a5d6ff">&#39;[data-test=&#34;email&#34;]&#39;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> email.trigger(<span style="color:#a5d6ff">&#34;focus&#34;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> email.<span style="color:#ff7b72">get</span>(<span style="color:#a5d6ff">&#34;input&#34;</span>).setValue(formData.email);
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> email.trigger(<span style="color:#a5d6ff">&#34;blur&#34;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> password <span style="color:#ff7b72;font-weight:bold">=</span> wrapper.getComponent(<span style="color:#a5d6ff">&#39;[data-test=&#34;password&#34;]&#39;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> password.trigger(<span style="color:#a5d6ff">&#34;focus&#34;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> password.<span style="color:#ff7b72">get</span>(<span style="color:#a5d6ff">&#34;input&#34;</span>).setValue(formData.password);
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> password.trigger(<span style="color:#a5d6ff">&#34;blur&#34;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#8b949e;font-style:italic">// 使用真實的 Router 測試時，若想要捕捉 Push 的行為，須透過 vi.spyOn 來模擬 router.push
</span></span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> spyPush <span style="color:#ff7b72;font-weight:bold">=</span> vi.spyOn(router, <span style="color:#a5d6ff">&#34;push&#34;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> wrapper.<span style="color:#ff7b72">get</span>(<span style="color:#a5d6ff">&#39;[data-test=&#34;loginBtn&#34;]&#39;</span>).trigger(<span style="color:#a5d6ff">&#34;click&#34;</span>);
</span></span><span style="display:flex;"><span>  expect(spyPush).toHaveBeenCalledTimes(<span style="color:#a5d6ff">1</span>);
</span></span><span style="display:flex;"><span>  expect(spyPush).toHaveBeenCalledWith(<span style="color:#a5d6ff">&#34;/home&#34;</span>);
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><p>在文章的最後，筆者也放一下所有測試的結果圖（可透過套件 or 指令 <code>yarn test</code> 執行）：</p>
<p><img src="https://deanlin.net/blog/202301/vue-test-utils-examples/images/vue-test-utils-examples-05.webp" width="1400" height="1192" alt="︎ 模擬使用者 trigger button 事件，觀察頁面是否如預期跳轉（Vue Router）（圖 5）" loading="lazy" decoding="async"></p>
<p>今天的文章就分享到這邊，我們下次再見～</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>
  </channel>
</rss>
