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

Vue3+Vitest测试报错:$setup.formatViews不是函数,如何解决?

问题:Vue3组件Vitest测试报错 TypeError: $setup.formatViews is not a function

我正在用Vitest为Vue3组件编写测试,组件和测试代码如下,运行测试时触发错误:

TypeError: $setup.formatViews is not a function
❯ Proxy._sfc_render src/components/UI/VideoPlayer.vue:81:13

不确定是否需要mock $setup.formatViews,该如何解决?

组件代码(VideoPlayer.vue)

<script lang="ts" setup>
import { reactive, computed,  ref, onMounted } from 'vue'
import { formatSubscribers } from '../../helpers/formatSubscribers'
import { formatDescription } from '../../helpers/formatDescription'
import { formatViews } from '../../helpers/formatViews'
import type {PropType} from 'vue'
import { useHistory } from '../../stores/history'
import type { IVideo } from '@/types/video'

const historyStore = useHistory()

interface State {
  readMore: boolean
}

const state = reactive<State>({
  readMore: false
})

const props = defineProps({
  videoId: { type: String, required: true },
  video: { type: Object as PropType<IVideo>, required: true }
})

const embedUrl = computed(
  () => `https://www.youtube.com/embed/${props.videoId}?autoplay=1&showinfo=1&mute=0`
)

const channelThumbnailUrl = computed(() => props.video?.channel?.thumbnail?.url)

const formattedSubscriberCount = computed(() => {
  const subscriberCount = props.video?.channel?.subscriberCount
  return formatSubscribers(subscriberCount) + ' inscritos'
})

const descriptionHtml = computed(() => {
  return formatDescription(props.video.description, state.readMore)
})

const readMoreButtonLabel = computed(() => {
  return state.readMore ? 'ver menos' : 'ver mais'
})

function toggleReadMore() {
  state.readMore = !state.readMore
}

function addVideo() {
  const history = historyStore.getHistory
  if (history.length === 0 || history[0].id !== props.video.id) {
    historyStore.addVideo(props.video)
  }
}

const videoPlayer = ref<HTMLIFrameElement | null>(null)
onMounted(() => {
  videoPlayer.value?.addEventListener('load', addVideo)
})
</script>

<template>
  <div class="video">
    <div class="iframe-container">
      <iframe
        ref="videoPlayer"
        :src="embedUrl"
        frameborder="0"
        allowfullscreen
        v-if="video"
      ></iframe>
    </div>
    <h1 class="video__name">{{ video.title }}</h1>
    <div class="video__channel">
      <img :src="channelThumbnailUrl" :alt="video.channel.title" />
      <div>
        <p>{{ video.channel.title }}</p>
        <p>{{ formattedSubscriberCount }}</p>
      </div>
    </div>
    <div class="video__description">
      <p>{{ formatViews(video.viewCount) }}</p>
      <p v-html="descriptionHtml"></p>
      <button @click="toggleReadMore">{{ readMoreButtonLabel }}</button>
    </div>
  </div>
</template>
<style lang="scss" scoped>
.iframe-container {
  position: relative;
  overflow: hidden;
  width: 100%;
  padding-top: 56.25%;

  iframe {
    position: absolute;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
    width: 100%;
    height: 100%;
  }
}

.video {
  padding: 64px 0;

  @media (min-width: 768px) {
    padding: 128px 0;
  }

  &__name {
    margin-top: 14px;
    margin-bottom: 14px;
    font-size: 20px;
  }

  &__channel {
    display: flex;
    gap: 14px;
    margin-bottom: 14px;

    img {
      border-radius: 100%;
      width: 40px;
      height: 40px;
    }
  }

  &__description {
    padding: 14px;
    border-radius: 8px;
    background-color: #282828;
    p:first-of-type {
      margin-bottom: 14px;
    }
    button {
      height: 48px;
      padding: 8px;
      margin-top: 14px;
      border: none;
      border-radius: 8px;
      cursor: pointer;
      color: inherit;
      background-color: transparent;
      background-color: hsla(0, 0%, 100%, 0.08);
      border: 1px solid hsl(0, 0%, 18.82%);
      text-transform: capitalize;
      &:hover {
        background-color: hsla(0, 0%, 100%, 0.16);
      }
    }
  }
}
</style>

测试代码

import { mount } from '@vue/test-utils'
import { describe, it,expect } from 'vitest'
import VideoPlayer from '@/components/UI/VideoPlayer.vue'
import type { IVideo } from '@/types/video';
import { formatViews } from '@/helpers/formatViews';

const videoMock: IVideo = {
  id: 'abc123',
  title: 'Test Video',
  description: 'This is a test video.',
  thumbnail: 'https://example.com/thumbnail.png',
  channel: {
    id: 'def456',
    title: 'Test Channel',
    thumbnail: {
      url: 'https://example.com/channel_thumbnail.png',
      width: 100,
      height: 100,
    },
    subscriberCount: '1000',
  },
  viewCount: '100',
  duration: '1:00',
};

describe('Video', () => {
  it('displays the video title', () => {
    const wrapper = mount(VideoPlayer, { props: { video: videoMock, videoId: '1' } })
    expect(wrapper.find('.video__name').text()).toBe('Test Video')
  })

  it('displays the channel title and subscriber count', () => {
    const wrapper = mount(VideoPlayer, { props: { video: videoMock, videoId: '1' } })
    expect(wrapper.find('.video__channel p:first-child').text()).toBe('Test Channel')
    expect(wrapper.find('.video__channel p:last-child').text()).toBe('1,000')
  })

})

解决方案

这个错误确实是因为测试环境未正确处理组件导入的工具函数导致的,同时组件依赖的Pinia Store也需要mock,以下是完整解决步骤:

1. Mock所有导入的工具函数

使用Vitest的vi.mock方法mock组件依赖的工具函数,确保组件能调用到mock后的函数:

2. Mock Pinia Store

组件中使用了useHistory,测试时必须mock这个Store,否则会因找不到Pinia实例报错。

修正后的测试代码

import { mount } from '@vue/test-utils'
import { describe, it, expect, vi } from 'vitest'
import VideoPlayer from '@/components/UI/VideoPlayer.vue'
import type { IVideo } from '@/types/video';

// Mock formatViews工具函数
vi.mock('@/helpers/formatViews', () => ({
  formatViews: (count: string) => `100 visualizações` // 自定义返回值,匹配业务逻辑或方便断言
}))

// Mock formatSubscribers工具函数
vi.mock('@/helpers/formatSubscribers', () => ({
  formatSubscribers: (count: string) => count.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}))

// Mock formatDescription工具函数
vi.mock('@/helpers/formatDescription', () => ({
  formatDescription: (desc: string, readMore: boolean) => readMore ? desc : `${desc.slice(0, 20)}...`
}))

// Mock Pinia的useHistory Store
vi.mock('@/stores/history', () => ({
  useHistory: () => ({
    getHistory: [],
    addVideo: vi.fn()
  })
}))

const videoMock: IVideo = {
  id: 'abc123',
  title: 'Test Video',
  description: 'This is a test video.',
  thumbnail: 'https://example.com/thumbnail.png',
  channel: {
    id: 'def456',
    title: 'Test Channel',
    thumbnail: {
      url: 'https://example.com/channel_thumbnail.png',
      width: 100,
      height: 100,
    },
    subscriberCount: '1000',
  },
  viewCount: '100',
  duration: '1:00',
};

describe('VideoPlayer', () => {
  it('displays the video title', () => {
    const wrapper = mount(VideoPlayer, { props: { video: videoMock, videoId: '1' } })
    expect(wrapper.find('.video__name').text()).toBe('Test Video')
  })

  it('displays the channel title and formatted subscriber count', () => {
    const wrapper = mount(VideoPlayer, { props: { video: videoMock, videoId: '1' } })
    expect(wrapper.find('.video__channel p:first-child').text()).toBe('Test Channel')
    // 注意原组件中拼接了'inscritos',修正断言内容
    expect(wrapper.find('.video__channel p:last-child').text()).toBe('1,000 inscritos')
  })

  it('displays formatted view count', () => {
    const wrapper = mount(VideoPlayer, { props: { video: videoMock, videoId: '1' } })
    expect(wrapper.find('.video__description p:first-of-type').text()).toBe('100 visualizações')
  })
})

额外说明

  • 如果工具函数是纯函数且无复杂依赖,也可以直接导入真实函数测试,但需要确保测试环境能正确解析模块路径(比如别名配置正确)。
  • 原测试中的断言expect(wrapper.find('.video__channel p:last-child').text()).toBe('1,000')会失败,因为组件中formattedSubscriberCount拼接了' inscritos',必须修正断言内容。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 10:20:03