This tutorial teaches you how to write unit and integration tests for auth flows in Vue apps using Jest, Vue Test Utils, and mocking strategies (mock tokens, axios mocks, fake backend handlers). I’ll give you copy-pasteable tests and explain design decisions so you can adapt to your stack (Pinia/Vuex, axios, fetch, Express).
Table of contents [Show]
- What you will learn
- Assumptions & prerequisites
- High-level testing strategy
- Project skeleton & minimal auth code
- Tools & libraries for mocking
- Unit tests: testing store logic with mocked axios
- Integration tests: mount components & simulate user flows
- Testing edge cases & attack scenarios
- Practical tips for writing reliable tests
- Full example repository structure (recommended)
- Summary & checklist
What you will learn
- How to structure tests for auth flows (unit vs integration).
- How to mock axios/HTTP calls and JWT tokens safely.
- Testing access token expiry + refresh flow with axios interceptor.
- Testing logout and refresh token revocation behavior.
- Simulating token reuse and ensuring your app reacts properly.
- Example code for a real-world auth store and test suite.
Assumptions & prerequisites
- Vue 3 project (Vite or Vue CLI) with Jest set up.
- Jest, @vue/test-utils, and axios in your devDependencies.
- Familiarity with Pinia or Vuex (examples use a simple store pattern—adaptable).
High-level testing strategy
Break testing into two levels:
- Unit tests — test isolated pieces (auth store actions, helper utilities, token parsing). Use mocks for HTTP calls. Fast and focused.
- Integration tests — mount components or a small app and test end-to-end behavior including axios interceptor, refresh flow, and UI updates. Use HTTP-level mocking (axios mock adapter or MSW) to simulate server responses.
Project skeleton & minimal auth code
To keep examples concrete, here’s a minimal auth store and axios client that our tests will exercise. If you already use Pinia/Vuex, adapt the logic accordingly.
File: src/services/api.js (axios client with refresh interceptor)
// src/services/api.js
import axios from 'axios'
import { authStore } from '../stores/auth' // adjust to your store system
const api = axios.create({ baseURL: '/api', withCredentials: true })
let isRefreshing = false
let failedQueue = []
function processQueue(error, token = null) {
failedQueue.forEach(p => {
if (error) p.reject(error)
else p.resolve(token)
})
failedQueue = []
}
api.interceptors.response.use(
r => r,
async err => {
const originalRequest = err.config
if (err.response && err.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true
const store = authStore()
if (isRefreshing) {
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject })
}).then(token => {
originalRequest.headers.Authorization = `Bearer ${token}`
return api(originalRequest)
})
}
isRefreshing = true
try {
const { data } = await axios.post('/api/auth/refresh', {}, { withCredentials: true })
store.setAccessToken(data.accessToken)
processQueue(null, data.accessToken)
originalRequest.headers.Authorization = `Bearer ${data.accessToken}`
return api(originalRequest)
} catch (e) {
processQueue(e, null)
store.logout()
return Promise.reject(e)
} finally {
isRefreshing = false
}
}
return Promise.reject(err)
}
)
export default api
File: src/stores/auth.js (simple store-ish module)
// src/stores/auth.js
import { ref } from 'vue'
export function authStore() {
// in-memory access token for security in examples
const accessToken = ref(null)
const user = ref(null)
const setAccessToken = token => { accessToken.value = token }
const setUser = u => { user.value = u }
const logout = () => { accessToken.value = null; user.value = null }
return { accessToken, user, setAccessToken, setUser, logout }
}
The tests will mock axios network responses and validate that the store responds accordingly (tokens set, logout called, etc.). You can replace authStore() with a Pinia store and the same tests apply with small adaptions.
Tools & libraries for mocking
- jest.mock() — quick way to mock axios module behavior.
- axios-mock-adapter — useful for mocking axios requests at HTTP-level.
- msw (Mock Service Worker) — excellent for integration tests that simulate server behavior at network layer; works both on Node and the browser.
- vue-test-utils — for mounting components and integration tests in DOM.
Unit tests: testing store logic with mocked axios
We will write unit tests that:
- Test successful login sets access token and user.
- Test failed login yields error and doesn’t set token.
- Test refresh flow upon 401: when access token is expired, interceptor calls refresh and original request is retried.
- Test logout clears store when refresh fails.
Install test deps
npm install --save-dev jest @vue/test-utils axios-mock-adapterUnit test file: tests/unit/auth.store.spec.js
// tests/unit/auth.store.spec.js
import axios from 'axios'
import MockAdapter from 'axios-mock-adapter'
import api from '@/services/api'
import { authStore } from '@/stores/auth'
jest.mock('@/stores/auth', () => {
// return a fresh store per import to isolate tests
return {
authStore: jest.fn(() => {
const { ref } = require('vue')
const accessToken = ref(null)
const user = ref(null)
const setAccessToken = token => { accessToken.value = token }
const setUser = u => { user.value = u }
const logout = jest.fn(() => { accessToken.value = null; user.value = null })
return { accessToken, user, setAccessToken, setUser, logout }
})
}
})
describe('Auth flows (unit)', () => {
let mock
beforeEach(() => {
mock = new MockAdapter(axios)
})
afterEach(() => {
mock.restore()
jest.resetModules()
jest.clearAllMocks()
})
it('login sets token and user', async () => {
// simulate login endpoint
mock.onPost('/api/auth/login').reply(200, { accessToken: 'at-123', user: { id: 'u1', email: 'a@a' } })
const store = authStore()
// login implementation (example) calling axios
const res = await axios.post('/api/auth/login', { email: 'a', password: 'p' })
store.setAccessToken(res.data.accessToken)
store.setUser(res.data.user)
expect(store.accessToken.value).toBe('at-123')
expect(store.user.value).toEqual({ id: 'u1', email: 'a@a' })
})
it('refresh flow retries failed request and sets new token', async () => {
// Simulate: first request to /api/protected returns 401,
// refresh returns new accessToken, and original endpoint succeeds when retried.
// axios.post('/api/auth/refresh') -> returns new access token
mock.onPost('/api/auth/refresh').reply(200, { accessToken: 'new-token' })
// Protected endpoint: first call returns 401, second call returns 200
let callCount = 0
mock.onGet('/api/protected').reply(() => {
callCount++
if (callCount === 1) return [401]
return [200, { data: 'ok' }]
})
const store = authStore()
// initial token expired
store.setAccessToken('expired-token')
// perform request using api client which has interceptor
const response = await api.get('/api/protected').then(r => r.data)
// after interceptor, store accessToken should be updated via the refresh response
expect(store.accessToken.value).toBe('new-token')
expect(response).toEqual({ data: 'ok' })
})
it('failed refresh triggers logout', async () => {
// Make refresh return 401 => logout called
mock.onPost('/api/auth/refresh').reply(401)
// Protected endpoint returns 401
mock.onGet('/api/protected2').reply(401)
const store = authStore()
store.setAccessToken('expired-token')
await expect(api.get('/api/protected2')).rejects.toBeTruthy()
// logout should have been called by interceptor on refresh failure
expect(store.logout).toHaveBeenCalled()
})
})
Notes on unit tests:
- We used
axios-mock-adapterto intercept axios calls made by the client under test. - We mocked the store module to return isolated instances so tests don't share state across runs.
- We exercise the refresh interceptor behavior by forcing a 401 then responding to the
/auth/refreshcall.
Integration tests: mount components & simulate user flows
Integration tests validate how components, store, and HTTP clients work together. We'll use @vue/test-utils to mount a login component and MSW (mock service worker) to simulate a backend that supports rotation behavior.
Why MSW?
MSW intercepts network calls at the networking layer. It's excellent for integration tests because handlers can maintain state (simulate DB/Redis) and you can test sequential scenarios like login & subsequent refresh attempts.
Install MSW and test utils
npm install --save-dev msw @vue/test-utils @testing-library/jest-domExample server handlers (tests/setup/msw.js)
// tests/setup/msw.js
const { setupServer } = require('msw/node')
const { rest } = require('msw')
const jwt = require('jsonwebtoken')
// in-memory "refresh store"
const refreshStore = new Map()
function signAccess(uid) {
return jwt.sign({ uid }, 'access-secret', { expiresIn: '1m' })
}
function signRefresh(sid, uid) {
return jwt.sign({ sid, uid }, 'refresh-secret', { expiresIn: '7d' })
}
const server = setupServer(
rest.post('/api/auth/login', (req, res, ctx) => {
const { email } = req.body
const uid = email === 'admin@example.com' ? 'admin' : 'user'
const access = signAccess(uid)
const sid = Math.random().toString(36).slice(2)
const refresh = signRefresh(sid, uid)
refreshStore.set(sid, { uid, hash: refresh })
// set cookie simulation by returning refresh in body (for test convenience)
return res(ctx.status(200), ctx.json({ accessToken: access, refreshToken: refresh, user: { id: uid } }))
}),
rest.post('/api/auth/refresh', (req, res, ctx) => {
const token = req.cookies?.refreshToken || req.body?.refreshToken || req.headers.get('authorization')?.split(' ')[1]
if (!token) return res(ctx.status(401))
try {
const payload = jwt.verify(token, 'refresh-secret')
const stored = refreshStore.get(payload.sid)
if (!stored || stored.hash !== token) {
// reuse detection
refreshStore.clear() // revoke all
return res(ctx.status(401), ctx.json({ error: 'reuse' }))
}
// rotate: create new sid/token
const newSid = Math.random().toString(36).slice(2)
const newRefresh = signRefresh(newSid, payload.uid)
refreshStore.delete(payload.sid)
refreshStore.set(newSid, { uid: payload.uid, hash: newRefresh })
const newAccess = signAccess(payload.uid)
return res(ctx.status(200), ctx.json({ accessToken: newAccess, refreshToken: newRefresh }))
} catch (err) {
return res(ctx.status(401))
}
}),
rest.get('/api/protected', (req, res, ctx) => {
const auth = req.headers.get('authorization')
if (!auth) return res(ctx.status(401))
const token = auth.replace('Bearer ', '')
try {
jwt.verify(token, 'access-secret')
return res(ctx.status(200), ctx.json({ data: 'protected' }))
} catch (e) {
return res(ctx.status(401))
}
})
)
module.exports = { server, rest }
Integration test: tests/integration/auth.flow.spec.js
// tests/integration/auth.flow.spec.js
import { mount } from '@vue/test-utils'
import { server } from './setup/msw'
import { rest } from 'msw'
import api from '@/services/api'
import { authStore } from '@/stores/auth'
// start/stop MSW server
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
jest.mock('@/stores/auth', () => {
return {
authStore: jest.fn(() => {
const { ref } = require('vue')
const accessToken = ref(null)
const user = ref(null)
const setAccessToken = token => { accessToken.value = token }
const setUser = u => { user.value = u }
const logout = jest.fn(() => { accessToken.value = null; user.value = null })
return { accessToken, user, setAccessToken, setUser, logout }
})
}
})
describe('Integration: login -> access protected -> refresh rotation', () => {
it('login returns tokens and protected endpoint is accessible, and refresh rotates tokens', async () => {
// Simulate login by calling login endpoint
const res = await api.post('/api/auth/login', { email: 'user@example.com', password: 'x' })
const store = authStore()
store.setAccessToken(res.data.accessToken)
store.setUser(res.data.user)
// Access protected endpoint -> should succeed with access token
api.defaults.headers.common['Authorization'] = `Bearer ${store.accessToken.value}`
const protectedResp = await api.get('/api/protected')
expect(protectedResp.data).toEqual({ data: 'protected' })
// Simulate server rotating refresh token when refresh endpoint called
// Call refresh to get new access token
const refreshResp = await api.post('/api/auth/refresh', { refreshToken: res.data.refreshToken })
// store should update token
store.setAccessToken(refreshResp.data.accessToken)
expect(store.accessToken.value).toBe(refreshResp.data.accessToken)
// If we try to use old refresh token again, server will detect reuse and revoke
const oldRefresh = res.data.refreshToken
const reuseCall = await api.post('/api/auth/refresh', { refreshToken: oldRefresh }).catch(e => e.response)
expect(reuseCall.status).toBe(401)
})
})
Integration notes:
- MSW simulates server-side refresh token storage and rotation; tests call the real axios client so interceptors are exercised.
- For convenience in tests we pass refreshToken in response bodies; production should use HttpOnly cookies—MSW can simulate cookies if needed.
- This integration test validates happy path and a reuse detection case.
Testing edge cases & attack scenarios
Design tests that simulate attacks and failure modes so your app is resilient:
- Replay attack: call refresh with an old/rotated token and expect global logout and revocation behaviors.
- Concurrent refresh: simulate two simultaneous requests where interceptor queues requests until refresh completes—verify queue is processed and only one refresh call is made.
- Network failures: simulate refresh endpoint returning 500 and check app either retries (if intended) or logs out safely.
- Expired access token: simulate access token expiry and ensure interceptor tries refresh and retries original request.
Practical tips for writing reliable tests
- Isolate state: Ensure each test uses fresh store instances or re-initializes state between tests.
- Prefer integration tests for flows: Unit tests are fast, but real flow behaviors (interceptors, retries) should be covered by integration tests.
- Keep tests readable: Use helper functions to perform login, set tokens, and perform protected requests.
- Mock time where needed: If you test token expiry based on time, use
jest.useFakeTimers()and advance timers. - Avoid coupling to implementation: Test behaviors and outcomes (store updated, logout invoked), not internal private functions.
Full example repository structure (recommended)
/src
/services
api.js
/stores
auth.js
/tests
/unit
auth.store.spec.js
/integration
auth.flow.spec.js
/setup
msw.js
jest.config.js
package.json
Summary & checklist
To effectively test auth flows in your Vue app:
- Write unit tests for store logic and helpers, mocking HTTP calls.
- Write integration tests for full flows: login, call protected resource, refresh, token rotation, and logout.
- Use axios-mock-adapter or MSW for realistic HTTP mocking; prefer MSW for integration scenarios.
- Simulate edge cases: concurrent refreshes, reuse detection, network failures, expiry.
- Keep tests isolated and assert behavior (store state, logout calls, UI changes) rather than implementation details.






