Next.js starter your AI actually understands. Ship internal tools in days not weeks. Pre-order $199 $499 → [Get it now]

How to mock API in Vue tests

Mocking API calls in Vue tests ensures tests run fast, reliably, and without external dependencies. As the creator of CoreUI with over 10 years of Vue.js experience since 2014, I’ve written thousands of tests that mock API responses to verify component behavior under various scenarios. The most effective approach uses Vitest’s mocking utilities to mock fetch or axios calls, returning controlled responses for different test cases. This provides predictable, isolated tests that don’t depend on external services.

Mock fetch API calls using Vitest mock functions.

// UserList.vue
<template>
  <div>
    <p v-if="loading">Loading...</p>
    <ul v-else>
      <li v-for="user in users" :key="user.id">{{ user.name }}</li>
    </ul>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'

const users = ref([])
const loading = ref(true)

onMounted(async () => {
  const response = await fetch('/api/users')
  users.value = await response.json()
  loading.value = false
})
</script>
// UserList.spec.js
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import UserList from './UserList.vue'

describe('UserList with mocked API', () => {
  beforeEach(() => {
    global.fetch = vi.fn()
  })

  it('displays users after loading', async () => {
    const mockUsers = [
      { id: 1, name: 'John Doe' },
      { id: 2, name: 'Jane Smith' }
    ]

    global.fetch.mockResolvedValueOnce({
      json: async () => mockUsers
    })

    const wrapper = mount(UserList)
    expect(wrapper.text()).toContain('Loading...')

    await flushPromises()

    expect(wrapper.text()).toContain('John Doe')
    expect(wrapper.text()).toContain('Jane Smith')
    expect(wrapper.text()).not.toContain('Loading...')
  })
})

The vi.fn() creates a mock function. The mockResolvedValueOnce() returns a fake response object. The flushPromises() waits for async operations to complete. This tests API integration without making real HTTP requests.

Mocking Multiple API Calls

Test components that make sequential API requests.

// UserProfile.vue
<script setup>
import { ref, onMounted } from 'vue'

const user = ref(null)
const posts = ref([])
const loading = ref(true)

onMounted(async () => {
  const userResponse = await fetch('/api/user/1')
  user.value = await userResponse.json()

  const postsResponse = await fetch(`/api/user/${user.value.id}/posts`)
  posts.value = await postsResponse.json()

  loading.value = false
})
</script>
// UserProfile.spec.js
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import UserProfile from './UserProfile.vue'

describe('UserProfile with multiple API calls', () => {
  beforeEach(() => {
    global.fetch = vi.fn()
  })

  it('loads user and posts', async () => {
    const mockUser = { id: 1, name: 'John Doe' }
    const mockPosts = [
      { id: 1, title: 'First Post' },
      { id: 2, title: 'Second Post' }
    ]

    global.fetch
      .mockResolvedValueOnce({
        json: async () => mockUser
      })
      .mockResolvedValueOnce({
        json: async () => mockPosts
      })

    const wrapper = mount(UserProfile)
    await flushPromises()

    expect(wrapper.text()).toContain('John Doe')
    expect(wrapper.text()).toContain('First Post')
    expect(wrapper.text()).toContain('Second Post')

    expect(global.fetch).toHaveBeenCalledTimes(2)
    expect(global.fetch).toHaveBeenNthCalledWith(1, '/api/user/1')
    expect(global.fetch).toHaveBeenNthCalledWith(2, '/api/user/1/posts')
  })
})

Chaining mockResolvedValueOnce() returns different responses for sequential calls. The toHaveBeenNthCalledWith() verifies the correct endpoints were called. This tests complex data loading sequences.

Mocking Axios Requests

Mock axios instead of fetch for components using axios.

// ProductList.vue
<script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios'

const products = ref([])
const loading = ref(true)

onMounted(async () => {
  const response = await axios.get('/api/products')
  products.value = response.data
  loading.value = false
})
</script>
// ProductList.spec.js
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import axios from 'axios'
import ProductList from './ProductList.vue'

vi.mock('axios')

describe('ProductList with mocked axios', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  it('displays products from API', async () => {
    const mockProducts = [
      { id: 1, name: 'Product 1', price: 99 },
      { id: 2, name: 'Product 2', price: 149 }
    ]

    axios.get.mockResolvedValueOnce({
      data: mockProducts
    })

    const wrapper = mount(ProductList)
    await flushPromises()

    expect(wrapper.text()).toContain('Product 1')
    expect(wrapper.text()).toContain('Product 2')
    expect(axios.get).toHaveBeenCalledWith('/api/products')
  })

  it('handles API errors', async () => {
    axios.get.mockRejectedValueOnce(new Error('Network error'))

    const wrapper = mount(ProductList)
    await flushPromises()

    expect(wrapper.text()).toContain('Error')
  })
})

The vi.mock('axios') automatically mocks axios. The axios.get.mockResolvedValueOnce() returns mock data. The mockRejectedValueOnce() simulates errors. The clearAllMocks() resets mocks between tests.

Using Mock Service Worker (MSW)

Use MSW for more realistic API mocking with request handlers.

npm install --save-dev msw
// mocks/handlers.js
import { http, HttpResponse } from 'msw'

export const handlers = [
  http.get('/api/users', () => {
    return HttpResponse.json([
      { id: 1, name: 'John Doe', email: '[email protected]' },
      { id: 2, name: 'Jane Smith', email: '[email protected]' }
    ])
  }),

  http.post('/api/users', async ({ request }) => {
    const newUser = await request.json()
    return HttpResponse.json(
      { id: 3, ...newUser },
      { status: 201 }
    )
  }),

  http.get('/api/users/:id', ({ params }) => {
    const { id } = params
    return HttpResponse.json({
      id: Number(id),
      name: `User ${id}`,
      email: `user${id}@example.com`
    })
  })
]
// setup-tests.js
import { beforeAll, afterEach, afterAll } from 'vitest'
import { setupServer } from 'msw/node'
import { handlers } from './mocks/handlers'

const server = setupServer(...handlers)

beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
// UserManager.spec.js
import { describe, it, expect } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import UserManager from './UserManager.vue'

describe('UserManager with MSW', () => {
  it('loads and displays users', async () => {
    const wrapper = mount(UserManager)
    await flushPromises()

    expect(wrapper.text()).toContain('John Doe')
    expect(wrapper.text()).toContain('Jane Smith')
  })

  it('creates new user', async () => {
    const wrapper = mount(UserManager)
    await flushPromises()

    await wrapper.find('input[name="name"]').setValue('Bob Johnson')
    await wrapper.find('input[name="email"]').setValue('[email protected]')
    await wrapper.find('form').trigger('submit')
    await flushPromises()

    expect(wrapper.text()).toContain('Bob Johnson')
  })
})

MSW intercepts HTTP requests at the network level. Handlers define responses for different endpoints. The server setup runs for all tests. This provides realistic API mocking that works with any HTTP library.

Mocking API with Different Scenarios

Test error handling and edge cases with conditional mocks.

describe('UserList error scenarios', () => {
  beforeEach(() => {
    global.fetch = vi.fn()
  })

  it('handles 404 error', async () => {
    global.fetch.mockResolvedValueOnce({
      ok: false,
      status: 404,
      json: async () => ({ message: 'Not found' })
    })

    const wrapper = mount(UserList)
    await flushPromises()

    expect(wrapper.text()).toContain('Not found')
  })

  it('handles network error', async () => {
    global.fetch.mockRejectedValueOnce(new Error('Network failure'))

    const wrapper = mount(UserList)
    await flushPromises()

    expect(wrapper.text()).toContain('Network failure')
  })

  it('handles empty response', async () => {
    global.fetch.mockResolvedValueOnce({
      json: async () => []
    })

    const wrapper = mount(UserList)
    await flushPromises()

    expect(wrapper.text()).toContain('No users found')
  })

  it('handles timeout', async () => {
    global.fetch.mockImplementationOnce(
      () => new Promise((resolve) => setTimeout(resolve, 5000))
    )

    const wrapper = mount(UserList)

    // Test loading state persists during timeout
    expect(wrapper.text()).toContain('Loading...')
  })
})

Different mock implementations test various failure scenarios. The mockRejectedValueOnce() simulates network failures. Custom mock implementations test timeout behavior. This ensures components handle all edge cases gracefully.

Mocking API with Composables

Test Vue composables that make API calls.

// composables/useUsers.js
import { ref } from 'vue'

export function useUsers() {
  const users = ref([])
  const loading = ref(false)
  const error = ref(null)

  async function fetchUsers() {
    loading.value = true
    error.value = null
    try {
      const response = await fetch('/api/users')
      users.value = await response.json()
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }

  return { users, loading, error, fetchUsers }
}
// useUsers.spec.js
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { useUsers } from './useUsers'

describe('useUsers composable', () => {
  beforeEach(() => {
    global.fetch = vi.fn()
  })

  it('fetches users successfully', async () => {
    const mockUsers = [{ id: 1, name: 'John' }]
    global.fetch.mockResolvedValueOnce({
      json: async () => mockUsers
    })

    const { users, loading, fetchUsers } = useUsers()

    expect(loading.value).toBe(false)

    const promise = fetchUsers()
    expect(loading.value).toBe(true)

    await promise
    expect(loading.value).toBe(false)
    expect(users.value).toEqual(mockUsers)
  })

  it('handles errors', async () => {
    global.fetch.mockRejectedValueOnce(new Error('API Error'))

    const { error, fetchUsers } = useUsers()

    await fetchUsers()

    expect(error.value).toBe('API Error')
  })
})

Composables can be tested independently from components. The test calls composable functions directly. This verifies business logic separately from UI rendering. Composable tests are faster and more focused.

Best Practice Note

This is the same API mocking approach we use in CoreUI Vue projects to ensure reliable, fast test suites. Mock API calls at the appropriate level - use simple mocks for unit tests and MSW for integration tests. Always test both success and error scenarios to ensure robust error handling. Reset mocks between tests to prevent test interdependence. For complex API workflows, consider creating mock factories that generate consistent test data. When testing components with API calls, verify loading states, error states, and success states separately. For more about testing Vue components, see our guide on how to test Vue components with Vue Test Utils.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author