Vue applications can be tested using the Jest test runner and Vue Test Utils.
Tools
Test-Writing Flow
-
Select a module or component to be tested
-
Create a file named
<module-name>.spec.jsin the__tests__directory -
Import the thing you want to test
If Testing a Component
- Import
mount(preferred) orshallowMountfrom@vue/test-utils-
mountis preferred because it more closely mirrors how the app works -
shallowMountstubs any components referenced inside the component being tested -
You can manually stub individual components in
mountby passing an array of string component names in a second options argument with the keystubs.Example:
const wrapper = mount(App, { stubs: ['router-view', 'vue-progress-bar'] });
-
- Import the component
If Testing a Module
- Import functions or objects to test from the module
- Import
-
Import
configfrom@vue/test-utilsif you need to check calls to one of the global mocks defined injest.setup.js -
Import any modules imported in the tested module that you will need to mock or spy on
-
Start with an outer
describeblock naming the module or component being tested (i.e.,describe('UserSingle component', () => {...});) -
Add any setup required for all tests of the module or component in this describe block
- Put as much as possible in a
beforecallback, especially if your test is async because otherwise all setup directly in thedescribecallbacks for all tests will run before any of the tests expectations are evaluated
- Put as much as possible in a
-
Continue adding
describeblocks and their relevant setup to create logical groupings for tests -
Add an
itcall, passing a string describing of the expectation and a callback that performs any actions specific to this one test and includes an expectation- A guideline of a single
expectcall peritcall helps maintain atomic tests - This ensures that, if a test fails, you don’t have to guess about what exactly needs to be fixed
- A guideline of a single
Example Component Test
describe('UserSingle component', () => {
const makeUserCopySpy = jest.spyOn(UserSingle.methods, 'makeUserCopy');
const mockUser = {};
const wrapper = mount(UserSingle, {
propsData: {
user: mockUser
},
stubs: ['User', 'ContentToolbar']
});
test(`calls makeUserCopy`, () => {
expect(makeUserCopySpy).toBeCalled();
});
});Example Component Test Suite
import {mount, config} from '@vue/test-utils';
import UserSingle from '../src/components/UserSingle.vue';
import firebase from 'firebase/app';
describe('UserSingle component', () => {
const makeUserCopySpy = jest.spyOn(UserSingle.methods, 'makeUserCopy');
const mockUser = {};
const wrapper = mount(UserSingle, {
propsData: {
user: mockUser
},
stubs: ['User', 'ContentToolbar']
});
const updateUserMock = jest.fn().mockResolvedValue();
const httpsCallableMock = jest.fn(functionName => {
if (functionName === 'updateUser') {
return updateUserMock;
}
});
jest.mock('firebase');
firebase.functions = jest
.fn()
.mockReturnValue({httpsCallable: httpsCallableMock});
test(`calls makeUserCopy`, () => {
expect(makeUserCopySpy).toBeCalled();
});
test(`userEditCopy is not original user`, () => {
expect(wrapper.vm.userEditCopy).not.toBe(mockUser);
});
describe('stopEdit method', () => {
beforeEach(() => {
jest.clearAllMocks();
});
const commitUserUpdatesSpy = jest.spyOn(wrapper.vm, 'commitUserUpdates');
test(`calls commitUserUpdates when save is true`, async () => {
await wrapper.vm.stopEdit(true);
expect(commitUserUpdatesSpy).toBeCalled();
});
test(`does not call commitUserUpdates when save is false`, async () => {
await wrapper.vm.stopEdit();
expect(commitUserUpdatesSpy).not.toBeCalled();
});
test(`starts progress bar`, async () => {
await wrapper.vm.stopEdit();
expect(config.mocks.$Progress.start).toBeCalled();
});
test(`finishes progress bar`, async () => {
await wrapper.vm.stopEdit();
expect(config.mocks.$Progress.finish).toBeCalled();
});
test(`finishes progress bar after save when save is true`, async () => {
await wrapper.vm.stopEdit(true);
expect(config.mocks.$Progress.finish).toHaveBeenCalledAfter(
commitUserUpdatesSpy
);
});
});
describe('commitUserUpdates method', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('calls updateUser Firebase Function', async () => {
await wrapper.vm.commitUserUpdates();
expect(updateUserMock).toBeCalled();
});
});
});Example Module Test
import {buildCheckAuthentication} from '../src/route-guards';
import {config} from '@vue/test-utils';
import firebaseApp from '../src/firebase-app';
const buildPromiseSpy = resolutionValue => jest.spyOn(firebaseApp);
describe('checkAuthentication guard', () => {
const mockRouter = {
app: {
$Progress: config.mocks.$Progress
}
};
const mockToSecureRoute = {
matched: [
{
meta: {
requiresAuth: true
}
}
],
name: 'secureRoute',
fullPath: 'https://test.com/original-destination'
};
const mockToRegularRoute = {
matched: [{meta: {}}],
name: 'regularRoute',
fullPath: 'https://test.com/original-destination'
};
const checkAuthentication = buildCheckAuthentication(mockRouter);
jest.spyOn(firebaseApp, 'getCurrentUser');
const nextMock = jest.fn();
beforeEach(() => {
jest.resetAllMocks();
});
describe('without a user authenticated', () => {
beforeEach(() => {
firebaseApp.getCurrentUser.mockResolvedValue(null);
});
describe('for a route requiring authentication', () => {
it('calls next redirecting to login route', async () => {
await checkAuthentication(mockToSecureRoute, null, nextMock);
expect(nextMock.mock.calls[0][0]).toMatchObject({name: 'login'});
});
});
});
});Example Module Test Suite
import {buildCheckAuthentication} from '../src/route-guards';
import {config} from '@vue/test-utils';
import firebaseApp from '../src/firebase-app';
const buildPromiseSpy = resolutionValue => jest.spyOn(firebaseApp);
describe('checkAuthentication guard', () => {
const mockRouter = {
app: {
$Progress: config.mocks.$Progress
}
};
const mockToSecureRoute = {
matched: [
{
meta: {
requiresAuth: true
}
}
],
name: 'secureRoute',
fullPath: 'https://test.com/original-destination'
};
const mockToRegularRoute = {
matched: [{meta: {}}],
name: 'regularRoute',
fullPath: 'https://test.com/original-destination'
};
const checkAuthentication = buildCheckAuthentication(mockRouter);
jest.spyOn(firebaseApp, 'getCurrentUser');
const nextMock = jest.fn();
beforeEach(() => {
jest.resetAllMocks();
});
describe('without a user authenticated', () => {
beforeEach(() => {
firebaseApp.getCurrentUser.mockResolvedValue(null);
});
describe('for a route requiring authentication', () => {
it('calls next redirecting to login route', async () => {
await checkAuthentication(mockToSecureRoute, null, nextMock);
expect(nextMock.mock.calls[0][0]).toMatchObject({name: 'login'});
});
});
describe('for a route not requiring authentication', () => {
it('calls next with no arguments', async () => {
await checkAuthentication(mockToRegularRoute, null, nextMock);
expect(nextMock).toHaveBeenCalledWith();
});
});
});
describe('with an established user authenticated', () => {
beforeEach(() => {
const mockUser = {claims: {}};
firebaseApp.getCurrentUser.mockResolvedValue(mockUser);
});
describe('for a route requiring authentication', () => {
it('calls next with no arguments', async () => {
await checkAuthentication(mockToSecureRoute, null, nextMock);
expect(nextMock).toHaveBeenCalledWith();
});
});
describe('for a route not requiring authentication', () => {
it('calls next with no arguments', async () => {
await checkAuthentication(mockToRegularRoute, null, nextMock);
expect(nextMock).toHaveBeenCalledWith();
});
});
});
describe('with a new user authenticated', () => {
beforeEach(() => {
const mockUser = {claims: {isNew: true}};
firebaseApp.getCurrentUser.mockResolvedValue(mockUser);
});
describe('routing to the setPassword route', () => {
const mockToSetPasswordRoute = {
matched: [{meta: {}}],
name: 'setPassword',
fullPath: 'https://test.com/original-destination'
};
it('calls next with no arguments', async () => {
await checkAuthentication(mockToSetPasswordRoute, null, nextMock);
expect(nextMock).toHaveBeenCalledWith();
});
});
describe('for a route requiring authentication', () => {
it('calls next redirecting to setPassword route', async () => {
await checkAuthentication(mockToSecureRoute, null, nextMock);
expect(nextMock.mock.calls[0][0]).toMatchObject({name: 'setPassword'});
});
});
describe('for a route not requiring authentication', () => {
it('calls next redirecting to setPassword route', async () => {
await checkAuthentication(mockToRegularRoute, null, nextMock);
expect(nextMock.mock.calls[0][0]).toMatchObject({name: 'setPassword'});
});
});
});
});Resources
Vue 2
- Vue Testing Handbook
- Using Vue Test Uitls with Vue Router- Very basic information about testing Vue Router and even testing around Vue Router (for when the router isn’t what you want to test). Great useful info, but not much meat here.
- Vue Router Testing Strategies- A much deeper dive on testing navigation guards.