You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Nuxt3中如何测试含表单、useFetch与Pinia的SSR页面?

Nuxt3 SSR登录页面测试问题

项目代码(pages/login.vue)

<template>
    <form
        @submit.prevent="login"
    >
        <input
            name="email"
            v-model="form.email"
        />
        <input
            name="password"
            v-model="form.password"
        />
        <button>login</button>
    </form>
</template>

<script setup>
import { useAuth } from '~/store/auth.js'; 

const auth = useAuth();
const form = ref({email: '', password: ''});
const router = useRouter();

const login = async () => {
    useFetch('/api/auth/tokens', {
        method: 'POST',
        body: form.value
    })
    .then(({ data }) => {
          const { token, user } =  data.value;
          auth.setUser(user);
          auth.setToken(token);
          router.push('/profile');           
    })
}
</script>

尝试的测试方式及问题

1. SPA模式测试(@vue/test-utils mount)

使用mount方法时出现document is not defined错误,且无法测试useFetch回调中的响应处理和页面跳转逻辑:

import { describe, test, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import LoginPage from '~/pages/login.vue';

describe('login page', async () => {
    test('store tokens', async () => {
        const wrapper = mount(LoginPage);
        const email = 'test@example.com';
        const password = 'password';
        await wrapper.find('[name="email"]').setValue(email);
        await wrapper.find('[name="password"]').setValue(password);
        await wrapper.find('form').trigger('submit.prevent');
        expect(wrapper.emitted('submit')[0][0]).toStrictEqual({email,password});
        // 无法测试then()中处理响应和跳转的代码
    });
});

2. Nuxt官方文档方式(@nuxt/test-utils + JSDOM)

通过setup和$fetch得到的是JSDOM文档实例,无法像Vue组件一样操作元素:

import { describe, test, expect } from 'vitest';
import { setup, $fetch } from '@nuxt/test-utils';
import { JSDOM } from 'jsdom';

describe('login page', async () => {
    await setup();
    test('store tokens', async () => {
        const wrapper = (new JSDOM(await $fetch('/login'))).window.document;
        const email = 'test@example.com';
        const password = 'password';
        await wrapper.find('[name="email"]').setValue(email);
        await wrapper.find('[name="password"]').setValue(password);
        await wrapper.find('form').trigger('submit.prevent');
        expect($fetch).toHaveBeenCalledWith('/api/auth/tokens', {
            method: 'POST',
            body: {email,password}
        });
    });
});

疑问

  1. 如何测试该页面的用户输入事件?
  2. 如何测试useFetch回调中处理Pinia数据的逻辑?

解决方案

1. 测试用户输入事件

使用@nuxt/test-utils提供的mount方法(而非@vue/test-utils原生mount),它会自动配置Nuxt运行环境,解决document is not defined问题,同时支持Vue组件式的元素操作:

import { describe, test, expect } from 'vitest';
import { mount } from '@nuxt/test-utils/runtime';
import LoginPage from '~/pages/login.vue';

describe('Login Page - Input Handling', () => {
  test('captures user input correctly', async () => {
    const wrapper = await mount(LoginPage);
    
    const emailInput = wrapper.find('[name="email"]');
    const passwordInput = wrapper.find('[name="password"]');
    
    await emailInput.setValue('test@example.com');
    await passwordInput.setValue('password');
    
    expect(wrapper.vm.form.email).toBe('test@example.com');
    expect(wrapper.vm.form.password).toBe('password');
  });
});

核心要点:

  • 依赖@nuxt/test-utils/runtime的mount,它会初始化Nuxt上下文(包含Pinia、路由等)
  • 通过wrapper.vm直接访问组件实例的form数据,验证双向绑定是否生效

2. 测试useFetch回调与Pinia逻辑

需要模拟useFetch的返回值,同时监听Pinia store方法和路由行为,验证回调逻辑是否正确执行:

import { describe, test, expect, vi } from 'vitest';
import { mount } from '@nuxt/test-utils/runtime';
import LoginPage from '~/pages/login.vue';
import { useAuth } from '~/store/auth.js';
import { ref } from 'vue';

// Mock useFetch 返回预设的成功响应
vi.mock('#app', () => ({
  useFetch: vi.fn(() => Promise.resolve({
    data: ref({
      token: 'mock-token-123',
      user: { id: 1, email: 'test@example.com' }
    })
  }))
}));

describe('Login Page - Auth Logic', () => {
  test('updates Pinia store and navigates on successful login', async () => {
    const wrapper = await mount(LoginPage);
    const authStore = useAuth();
    
    // 监听Pinia方法调用
    const mockSetUser = vi.spyOn(authStore, 'setUser');
    const mockSetToken = vi.spyOn(authStore, 'setToken');
    
    // 监听路由跳转
    const router = useRouter();
    const mockPush = vi.spyOn(router, 'push');
    
    // 先设置输入值
    await wrapper.find('[name="email"]').setValue('test@example.com');
    await wrapper.find('[name="password"]').setValue('password');
    
    // 触发表单提交
    await wrapper.find('form').trigger('submit.prevent');
    
    // 验证useFetch调用参数
    expect(useFetch).toHaveBeenCalledWith('/api/auth/tokens', {
      method: 'POST',
      body: { email: 'test@example.com', password: 'password' }
    });
    
    // 验证Pinia方法是否被正确调用
    expect(mockSetUser).toHaveBeenCalledWith({ id: 1, email: 'test@example.com' });
    expect(mockSetToken).toHaveBeenCalledWith('mock-token-123');
    
    // 验证路由跳转是否触发
    expect(mockPush).toHaveBeenCalledWith('/profile');
  });
});

核心要点:

  • 用vi.mock模拟useFetch接口,返回预设的响应数据,避免依赖真实后端
  • 通过vi.spyOn监听Pinia的setUser/setToken方法、路由的push方法,验证逻辑触发情况
  • 若需测试错误场景,只需修改useFetch的模拟返回值(比如返回reject状态)

内容的提问来源于stack exchange,提问作者DengSihan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 22:50:31