iT邦幫忙

2024 iThome 鐵人賽

DAY 23
1
Modern Web

Vue 和 TypeScript 的最佳實踐:成為前端工程師的進階利器系列 第 23

Day 23: 如何測試 Vue Router 的導航邏輯與 Pinia 的狀態管理

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20240925/20117461iGxz4WeWXd.jpg

簡介

在 Vue 3 應用程序中,Vue Router 和 Pinia 是兩個核心工具,分別用於處理路由導航和狀態管理。對這兩個工具進行有效的測試對於確保應用程序的穩定性和可靠性至關重要。本文將探討如何使用 Vitest 和 @vue/test-utils 來測試 Vue Router 的導航邏輯和 Pinia 的狀態管理。

步驟 1: 創建測試用的 Router 和 Pinia Store

讓我們創建一個簡單的路由配置和 Pinia store 用於測試:

(檔案 : src/router/index.ts)

import { createRouter, createWebHistory, } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
import { useAuthStore } from '../stores/useAuthStore';
import { storeToRefs } from 'pinia';
 
export enum RoutesStatus {
  Home = 'Home',
  SignIn = 'SignIn',
  Auth = 'auth',
  NotFound = 'NotFound',
  TokenFail = 'TokenFail',
}

export const routes: RouteRecordRaw[] = [ 
  { 
    path: '/', 
    component: () => import('../pages/Dashboard.vue'),
    children: [
      {
        path: '',
        name: RoutesStatus.Home,
        component: () => import('../pages/Home.vue'),
      },
      {
        path: '/signIn',
        name: RoutesStatus.SignIn,
        component: () => import('../pages/SignIn.vue'),
      },
      {
        path: 'auth',
        name: RoutesStatus.Auth,
        component: () => import('../pages/Auth.vue'),
        meta: {
          requiresAuth: true,
        }
      }
    ],
  },
  { 
    path: '/:catchAll(.*)',
    name: RoutesStatus.NotFound,
    component: () => import('../pages/NotFound.vue') 
  },
  { 
    path: '/tokenFail',
    name: RoutesStatus.TokenFail,
    component: () => import('../pages/TokenNotFound.vue') 
  },
] 

const router = createRouter({
  history: createWebHistory(),
  routes,
});

router.beforeEach(to => {
  const authStore = useAuthStore();
  const isAuth = to.meta.requiresAuth;
  if (!isAuth) return true;
  const { isAuthenticated } = storeToRefs(authStore);
  if (!isAuthenticated.value) return { name: RoutesStatus.SignIn };
  return true;
});

export default router;

(檔案src/stores/useAuthStore.ts)

備註 : 不知道 definePrivateState 的部分可以在 day11 找到

import { computed } from 'vue';
import { acceptHMRUpdate } from 'pinia';
import { definePrivateState } from './privateState';
import { UserSchema } from '../schemas/user.schema';

export interface UseAuthStorePrivateState {
  isAuthenticated: boolean;
  user: UserSchema | null;
}

export const useAuthStore = definePrivateState('useAuthStore', (): UseAuthStorePrivateState => {
  return {
    isAuthenticated: false,
    user: null,
  }
}, privateState => {
  // methods::
  const login = (userName: string, password: string): boolean => {
    // 模擬登入邏輯
    const isAuthOkay = userName === 'admin' && password === 'password';
    if (isAuthOkay) {
      privateState.isAuthenticated = true;
      privateState.user = { userName }
    }
    return isAuthOkay;
  };

  const logout = (): void => {
    privateState.isAuthenticated = false;
    privateState.user = null;
  };

  return {
    // getters::
    user: computed<UserSchema | null>(() => privateState.user),
    isAuthenticated: computed<boolean>(() => privateState.isAuthenticated),
    // methods::
    login,
    logout
  }
});

if (import.meta.hot) {
  import.meta.hot.accept(acceptHMRUpdate(useAuthStore, import.meta.hot));
}

步驟 2: 測試 Vue Router 導航邏輯

讓我們創建一個測試文件來測試路由導航邏輯:

(檔案 : src/router/router.test.ts)

import { describe, it, expect, beforeEach } from "vitest";
import { setActivePinia, createPinia, storeToRefs } from 'pinia';
import { createRouter, createWebHistory, Router } from 'vue-router'
import { routes, RoutesStatus } from ".";
import { useAuthStore } from "../stores/useAuthStore";

describe("Router Navigation Test", () => {
  let router: Router | null = null;
  let authStore: ReturnType<typeof useAuthStore> | null = null;

  beforeEach(() => {
    setActivePinia(createPinia());
    authStore = useAuthStore();
    router = createRouter({
      history: createWebHistory(),
      routes,
    })

    router.beforeEach(to => {
      const isAuth = to.meta.requiresAuth;
      if (!isAuth) return true;
      if (authStore === null) return { name: RoutesStatus.SignIn };
      const { isAuthenticated } = storeToRefs(authStore);
      if (!isAuthenticated.value) return { name: RoutesStatus.SignIn };
      return true;
    })

  });

  it('allows navigation to public routes', async () => {
    expect(router).not.toBeNull();
    if (router === null) return;

    await router.push({ name: RoutesStatus.Home });
    expect(router.currentRoute.value.path).toBe('/');
  })

  it('redirects to login when accessing protected route while not authenticated', async () => {
    expect(router).not.toBeNull();
    if (router === null) return;

    await router.push({ name: RoutesStatus.Auth });
    expect(router.currentRoute.value.path).toBe('/signIn');
    expect(router.currentRoute.value.name).toBe(RoutesStatus.SignIn);
  })

  it('allows navigation to protected route when authenticated', async () => {
    expect(router).not.toBeNull();
    expect(authStore).not.toBeNull();
    if (router === null) return;
    if (authStore === null) return;
    const { login } = authStore;

    login('admin', 'password')
    await router.push({ name: RoutesStatus.Auth });
    expect(router.currentRoute.value.path).toBe('/auth');
    expect(router.currentRoute.value.name).toBe(RoutesStatus.Auth);
  })
});

這個測試文件模擬了路由導航守衛的行為,並測試了公共路由和受保護路由的訪問邏輯。

步驟 3: 測試 Pinia Store

現在讓我們測試 Pinia store 的狀態管理:

(檔案: src/stores/useAuthStore.test.ts)


import { setActivePinia, createPinia, storeToRefs } from 'pinia';
import { describe, it, expect, beforeEach } from 'vitest';
import { useAuthStore } from './useAuthStore';

describe('useAuthStore.ts', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })

  it('initializes with correct values', () => {
    const authStore = useAuthStore();
    const { isAuthenticated, user } = storeToRefs(authStore);

    expect(isAuthenticated.value).toBe(false)
    expect(user.value).toBe(null)
  })

  it('logs in successfully with correct credentials', () => {
    const authStore = useAuthStore();
    const { login } = authStore;
    const { isAuthenticated, user } = storeToRefs(authStore);

    const result = login('admin', 'password')
    expect(result).toBe(true)
    expect(isAuthenticated.value).toBe(true)
    expect(user.value).toEqual({ userName: 'admin' })
  })

  it('fails to log in with incorrect credentials', () => {
    const authStore = useAuthStore();
    const { login } = authStore;
    const { isAuthenticated, user } = storeToRefs(authStore);

    const result = login('admin', 'wrongPassword')
    expect(result).toBe(false)
    expect(isAuthenticated.value).toBe(false)
    expect(user.value).toBe(null)
  })

  it('logs out correctly', () => {
    const authStore = useAuthStore();
    const { login, logout } = authStore;
    const { isAuthenticated, user } = storeToRefs(authStore);

    login('admin', 'password')
    logout()
    expect(isAuthenticated.value).toBe(false)
    expect(user.value).toBe(null)
  })
});

這個測試文件驗證了 auth store 的初始狀態、登入邏輯和登出功能。

結論

在本文中,我們深入探討了如何使用 Vitest 和 @vue/test-utils 來測試 Vue Router 的導航邏輯和 Pinia 的狀態管理。

通過這些測試,我們確保了:

  1. 路由導航邏輯正確處理公共路由和受保護路由
  2. Pinia store 正確管理認證狀態

加上之前 day 21 先前的表單驗證,幾乎 80% 和 vue 有關的測試都可以自己實現了。
未來會針對 api 進行 mock 測試,到時候最後一塊 19% 測試拼圖就拼成,敬請期待。
至於確實在單元測試有許多特殊情況,由於篇幅有限所以更深入的主題會在以後有機會在我的另外的文章出現。

在實際開發中,你可能需要根據具體的業務邏輯來調整和擴展這些測試。記住,好的測試不僅能捕捉錯誤,還能幫助你更好地理解和改進你的代碼。持續地編寫和維護測試,將有助於你構建更加健壯和可維護的 Vue 應用。

最後,在測試 Vue Router 和 Pinia 時,要特別注意異步操作的處理,確保在斷言之前所有的狀態更新都已完成。同時,善用 mock 和 spy 功能可以幫助你更好地隔離被測試的單元,提高測試的精確性和可靠性,這部分在未來篇幅會演示給大家看。


上一篇
Day 22: 使用 TypeScript 和 Vitest 測試 Vue 組件的邊界情況
下一篇
Day 24: 性能優化:如何利用 UnoCSS 與 Vite 減少打包大小還有優化 vue 的各式操作
系列文
Vue 和 TypeScript 的最佳實踐:成為前端工程師的進階利器30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言