ScreenshotNeo

BlogHow-to

Testing a Vue App With Vuex and a REST Backend in Cypress

Test Vuex state, user-visible behavior, and REST traffic in Cypress with fresh stores, controlled API stubs, and selected real-backend flows.

By the ScreenshotNeo team4 October 202613 min read

To test a Vue app with Vuex and a REST backend in Cypress, use component tests for isolated components, application end-to-end (E2E) tests with cy.intercept() for browser requests, and selected E2E tests against the real backend for integration confidence. Use cy.request() when the test should call an endpoint directly; it does not observe a request made by the app. For Vue component tests, install a newly created Vuex store on every mount so state cannot leak between tests.

This guide uses Vue 3, Vuex 4, Cypress Component Testing, and a REST endpoint at /api/items as an example. Adapt endpoint paths, data shapes, and selectors to your application. Cypress documents component testing support for Vue 3+ and Vue/Vite or Vue/Webpack setups; check its current configuration guide and your installed versions before copying setup details. See the Cypress Vue component testing overview.

1. Choose the test layer that answers your question

Test type What it covers Backend behavior Good for
Component test One component with its props, plugins, and fresh Vuex store Usually no real REST server Rendering, user interaction, and store-driven states
App E2E with a stub The running app and a browser-driven user path cy.intercept() supplies a controlled response Repeatable loading, empty, error, and success UI states
App E2E with the real backend The running app and its actual server integration App requests reach the server Checking that the UI, REST API, and test data work together
Direct API test An endpoint response without app-driven browser traffic cy.request() calls the server directly Endpoint status, response shape, and setup or cleanup calls

These layers complement one another. Stubs make specific UI states deterministic, while a real-backend path checks the integration that a stub cannot establish. A direct API test isolates an endpoint from the UI; it does not demonstrate that the app sends the request or responds correctly to it. Cypress describes this distinction in its network request guide and request command documentation.

2. Set up Vue component testing with a fresh Vuex store

A Vue component that reads from Vuex needs the store installed when Cypress mounts it. Make a store factory, then invoke it inside the custom mount command for each test. Avoid reusing a mutable singleton store across tests: one test’s mutations can otherwise affect another test.

Cypress’s Vue examples use a custom cy.mount() command and support Vue Test Utils interoperability. The following illustrative setup assumes your app exports a store factory. If it currently exports only a singleton, extract the store creation into a function that returns a new store instance.

// cypress/support/component.js
import { mount } from 'cypress/vue'
import { createStore } from 'vuex'
import { h } from 'vue'

Cypress.Commands.add('mount', (component, options = {}) => {
  const store = options.store ?? createStore({
    state: () => ({ items: [], status: 'idle' }),
    mutations: {
      setItems(state, items) {
        state.items = items
      },
      setStatus(state, status) {
        state.status = status
      },
    },
    actions: {},
  })

  const { store: _store, ...mountOptions } = options
  return mount(component, {
    ...mountOptions,
    global: {
      ...(mountOptions.global ?? {}),
      plugins: [store, ...(mountOptions.global?.plugins ?? [])],
    },
  })
})

The example accepts an optional store for tests that need a particular initial state. When supplying one, still create it in that test, rather than sharing it. The store is installed as a Vue plugin, and any other component plugins can be passed through global.plugins.

// cypress/support/component.d.ts
/// <reference types="cypress" />

declare namespace Cypress {
  interface Chainable {
    mount: typeof import('cypress/vue').mount
  }
}

Ensure your Cypress component support file is configured to load cypress/support/component.js in the project’s Cypress configuration. Cypress setup works out of the box with Vite in supported configurations and can use a custom Webpack configuration. See the current Vue component examples and configuration docs for the installed Cypress and bundler versions.

Example component and component test

// src/components/ItemList.vue
<script setup>
import { computed, onMounted } from 'vue'
import { useStore } from 'vuex'

const store = useStore()
const items = computed(() => store.state.items)
const status = computed(() => store.state.status)

onMounted(() => {
  if (status.value === 'idle') store.dispatch('loadItems')
})
</script>

<template>
  <p v-if="status === 'loading'">Loading items</p>
  <p v-else-if="status === 'error'" role="alert">Could not load items</p>
  <p v-else-if="items.length === 0">No items yet</p>
  <ul v-else>
    <li v-for="item in items" :key="item.id">{{ item.name }}</li>
  </ul>
</template>
// ItemList.cy.js
import { createStore } from 'vuex'
import ItemList from './ItemList.vue'

const makeStore = (items, status = 'loaded') => createStore({
  state: () => ({ items, status }),
  mutations: {},
  actions: {
    loadItems() {},
  },
})

describe('ItemList', () => {
  it('renders items from its Vuex store', () => {
    cy.mount(ItemList, {
      store: makeStore([{ id: 1, name: 'Example item' }]),
    })

    cy.contains('li', 'Example item').should('be.visible')
  })

  it('renders an empty state', () => {
    cy.mount(ItemList, { store: makeStore([]) })
    cy.contains('No items yet').should('be.visible')
  })

  it('renders an error state', () => {
    cy.mount(ItemList, { store: makeStore([], 'error') })
    cy.get('[role="alert"]').should('contain', 'Could not load items')
  })
})

This component test checks the component’s rendering against store state. Because its test store uses a no-op action and it does not issue HTTP traffic, it does not cover the REST request or the real Vuex action implementation. Test those behaviors in an app test or in a focused unit test for the store action.

3. Stub REST responses in an app E2E test

For an app test, register an intercept before visiting or interacting with the page. Match the method and URL deliberately, add an alias, trigger the request, then wait on that alias before making assertions about the request, response, or UI.

// cypress/e2e/items-stubbed.cy.js
describe('items page with a controlled REST response', () => {
  it('renders items returned by the API', () => {
    cy.intercept('GET', '/api/items', {
      statusCode: 200,
      body: [{ id: 1, name: 'Example item' }],
    }).as('getItems')

    cy.visit('/items')

    cy.wait('@getItems').then(({ request, response }) => {
      expect(request.method).to.equal('GET')
      expect(response.statusCode).to.equal(200)
      expect(response.body).to.deep.equal([
        { id: 1, name: 'Example item' },
      ])
    })

    cy.contains('li', 'Example item').should('be.visible')
  })
})

Here, cy.intercept() both supplies a response and observes the app’s request. If you only need to observe real app traffic, register the route with a handler and call req.continue(). The route alias still gives the test a synchronization point.

cy.intercept('GET', '/api/items', (req) => {
  req.continue()
}).as('getItems')

cy.visit('/items')
cy.wait('@getItems').its('response.statusCode').should('eq', 200)
cy.contains('li', 'Example item').should('be.visible')

Use a fixture when a response is shared test data. Fixtures are useful for stable, realistic payloads and for states that are difficult or expensive to arrange on a live service.

// cypress/fixtures/items.json
[
  { "id": 1, "name": "Example item" },
  { "id": 2, "name": "Another item" }
]

// In a test
cy.intercept('GET', '/api/items', { fixture: 'items.json' }).as('getItems')
cy.visit('/items')
cy.wait('@getItems')
cy.contains('li', 'Another item').should('be.visible')

Keep fixture shapes representative of the actual API. The application should run its real parsing and store-update path; a fixture that omits fields the server normally returns can hide assumptions, while an unrealistic payload can test the wrong behavior.

Cover success, empty, error, and network failure states

it('shows the empty state for an empty collection', () => {
  cy.intercept('GET', '/api/items', { statusCode: 200, body: [] }).as('getItems')
  cy.visit('/items')
  cy.wait('@getItems')
  cy.contains('No items yet').should('be.visible')
})

it('shows an API error state', () => {
  cy.intercept('GET', '/api/items', {
    statusCode: 500,
    body: { message: 'Temporary server error' },
  }).as('getItems')
  cy.visit('/items')
  cy.wait('@getItems')
  cy.get('[role="alert"]').should('be.visible')
})

it('shows a network failure state', () => {
  cy.intercept('GET', '/api/items', { forceNetworkError: true }).as('getItems')
  cy.visit('/items')
  cy.wait('@getItems')
  cy.get('[role="alert"]').should('be.visible')
})

These tests assume the app maps the relevant response and failure conditions to an alert. Adjust assertions to the interface users actually see. Cypress clears intercepts before each test, so define each route in the test or a per-test setup hook. See the intercept documentation.

Assert on request data for mutations

For a form submission, stub the mutation response and inspect the outgoing request after the user action. This checks that the app sent the expected payload and rendered the response.

it('submits a new item', () => {
  cy.intercept('POST', '/api/items', (req) => {
    expect(req.body).to.deep.equal({ name: 'New item' })
    req.reply({ statusCode: 201, body: { id: 3, name: 'New item' } })
  }).as('createItem')

  cy.visit('/items')
  cy.get('[name="name"]').type('New item')
  cy.contains('button', 'Add item').click()

  cy.wait('@createItem').its('response.statusCode').should('eq', 201)
  cy.contains('li', 'New item').should('be.visible')
})

4. Keep selected E2E flows connected to the real backend

A stubbed test proves how the app behaves for the response you supplied. It does not prove the server route exists, that its response matches the app’s expectations, or that authentication and persistence work together. Keep an appropriate real-backend path for the integration behavior you need confidence in.

Set up or seed test data explicitly, then visit the app without stubbing the request under test. Cypress’s Real World App example uses server responses for most cases and stubs selected states that are hard to create. That example illustrates a balance, not a universal requirement that every project use the same seeding strategy. See the network guide.

// cypress/e2e/items-real-backend.cy.js
describe('items page against the test backend', () => {
  beforeEach(() => {
    // Example only: replace this setup endpoint and payload with your test API.
    cy.request('POST', '/test-support/reset-items', {
      items: [{ id: 1, name: 'Seeded item' }],
    })
  })

  it('loads a server-provided item in the app', () => {
    cy.intercept('GET', '/api/items').as('getItems')
    cy.visit('/items')

    cy.wait('@getItems').then(({ response }) => {
      expect(response.statusCode).to.equal(200)
      expect(response.body).to.be.an('array')
    })

    cy.contains('li', 'Seeded item').should('be.visible')
  })
})

The reset route above is illustrative; it is not a Cypress endpoint. Use a test-only setup API, database seeding, or a test-data factory that your project actually provides. Do not point destructive setup at production data. Make the environment and test data predictable so failures can be reproduced.

5. Call an endpoint directly with cy.request()

Use cy.request() when the API itself is the subject of the test or when a direct request is useful for preparing test data. It runs outside the browser’s app-originated request path. It will not trigger cy.intercept() as though the Vue app had sent a request.

it('returns the item collection from the API', () => {
  cy.request({
    method: 'GET',
    url: '/api/items',
    headers: { Accept: 'application/json' },
  }).then(({ status, body, headers }) => {
    expect(status).to.equal(200)
    expect(headers['content-type']).to.include('application/json')
    expect(body).to.be.an('array')
  })
})

For a protected route, provide test credentials or an authorization header using the mechanism your test environment supports:

cy.request({
  method: 'GET',
  url: '/api/items',
  headers: { Authorization: `Bearer ${Cypress.env('testApiToken')}` },
}).its('status').should('eq', 200)

Store secrets in Cypress environment configuration or your CI secret manager; do not commit real tokens into test files. See Cypress’s cy.request() documentation for options and behavior.

6. Match routes precisely and understand intercept behavior

Match both the HTTP method and a sufficiently narrow URL. If you omit the method, an intercept may match multiple methods. Cypress supports exact strings, glob patterns, and regular expressions for URLs. Query strings and base URL configuration can affect what URL you see, so inspect the actual request if a route does not match.

// Exact path and method
cy.intercept('GET', '/api/items').as('getItems')

// A glob for a route family
cy.intercept('GET', '**/api/items*').as('getItems')

// Regex when the URL pattern needs it
cy.intercept({
  method: 'GET',
  url: /\/api\/items(?:\?.*)?$/,
}).as('getItems')

If multiple routes match one request, ordinary intercepts are considered in reverse definition order; routes marked with middleware: true run first. Avoid overlapping broad routes unless their order is intentional. Use a route handler to inspect, modify, continue, or reply to a request; use a static response or fixture for a controlled stub. A forced network error is different from an HTTP error response: the former has no normal HTTP response status, while the latter is a server response such as 500.

One easy-to-miss case is browser caching. If the browser satisfies a resource from cache, it does not reach the network interception layer, so the intercept may never fire. Check caching before assuming the app has a timing race. Cypress discusses this limitation in its network guide; depending on your environment, disable cache headers in the development server or remove relevant cache headers through an appropriate test intercept.

7. Troubleshoot common failures

Symptom Likely cause Fix
cy.wait('@getItems') times out The route was registered after the request, the method or URL pattern does not match, or the request never happened. Register before cy.visit() or the triggering action. Check the browser request’s method, full URL, and query string; narrow or correct the matcher.
The intercept does not fire, but the UI has data The browser may have served the response from cache. Inspect response/cache headers and test-server cache behavior. Configure the test environment or intercept to avoid a cached response.
A route unexpectedly catches another request The matcher omits the method or uses a broad glob; another matching intercept may also be in play. Specify the method, narrow the URL, and review intercept registration order, including middleware routes.
Store state appears in a later test A shared mutable Vuex store is being reused. Build a new store per test or per mount. Keep the factory invocation inside the mount path.
A component reports that no store is available The mount did not install the Vuex plugin, or a component requiring a router or another plugin was mounted without it. Install Vuex (and other required plugins) through the custom mount command or pass the required plugins for that test.
A component test fails while building or mounting The component-testing bundler configuration or framework version may not match the installed Cypress setup. Check the current Cypress Vue configuration for your Vue, Vite, or Webpack versions and inspect the component support file.
cy.request() succeeds but the app test still fails The direct API call does not exercise the browser app’s request path or UI handling. Use cy.intercept() to observe or control the app’s request, then assert on the resulting UI.
A 500 test behaves differently from a network-error test An HTTP failure is a response; a forced network error has no ordinary response body or status. Test each condition separately and assert the application’s intended UI behavior for each.
Real-backend tests pass locally but fail in CI Test data, service availability, credentials, or environment configuration may differ. Make setup explicit, use isolated test data and CI secrets, and preserve enough request/response context to diagnose the failing path.

8. Reliability, speed, and cost considerations

  • Keep the default suite deterministic. Stub responses for UI states such as empty results, validation failures, and server errors. A fixed response makes those conditions repeatable without relying on live service state.
  • Retain integration confidence. Select real-backend app flows that cover the server behavior important to your application, and arrange their data explicitly. Do not infer backend coverage from stubbed tests.
  • Choose the narrowest useful layer. Component tests avoid booting a full app for local component behavior. App E2E tests cover routing and user flows. Direct API tests isolate endpoint behavior. Each answers a different question.
  • Synchronize on behavior. Alias and wait for the relevant request rather than adding arbitrary delays. Cypress’s retryable assertions can wait for user-visible DOM conditions to become true.
  • Budget for environment work, not invented speed claims. The research sources provide no comparative runtime benchmark. Real-backend reliability depends on controlled data and an available test service; keep those dependencies explicit in CI.
  • Direct monetary costs depend on your setup. Cypress is installed as a development dependency, and these examples do not require a paid Cypress feature or external screenshot service. Infrastructure and service charges depend on your project and are not specified here.

9. Capture test evidence with ScreenshotNeo

When a failed UI state is easier to review visually, a screenshot can preserve the rendered page for debugging or a CI artifact. ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. It is not a Cypress test runner; use Cypress to drive and assert on the app, then capture a page when you need a visual artifact.

Or skip the browser setup

One GET request returns an image or PDF. See the ScreenshotNeo API documentation for request options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
  • Cookie banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
  • Bot checks, blank pages, failed loads, timeouts, and cache hits are never billed. Response headers say which page verdict occurred and whether it was billed.
  • An MCP server lets Claude, Cursor, or another MCP client use take_screenshot, get_page_info, and capture_pdf.
  • 1,000 screenshots per month are free with no card. Paid plans start at $5 for 3,000 screenshots. Every feature is on every plan.

Sign up for ScreenshotNeo and get 1,000 free screenshots a month with no card.

10. FAQ

Can I use Vuex with Cypress Component Testing?

Yes. Install a Vuex store when mounting the component. Create it fresh for each test so state changes remain isolated.

Does a stubbed API test verify the backend?

No. It verifies app behavior for the response supplied by the test. Include an appropriate real-backend path to exercise the actual integration.

Should I use cy.intercept() or cy.request()?

Use cy.intercept() for browser requests initiated by the app. Use cy.request() for a direct API call, such as an endpoint test or test-data setup.

Why did my intercept not catch the request?

Common causes include registering too late, a method or URL mismatch, or a cached browser response that never reaches the network layer.