Vue applications can be tested using the Jest test runner and Vue Test Utils.

Tools

Test-Writing Flow

  1. Select a module or component to be tested

  2. Create a file named <module-name>.spec.js in the __tests__ directory

  3. Import the thing you want to test

    If Testing a Component

    • Import mount (preferred) or shallowMount from @vue/test-utils
      • mount is preferred because it more closely mirrors how the app works

      • shallowMount stubs any components referenced inside the component being tested

      • You can manually stub individual components in mount by passing an array of string component names in a second options argument with the key stubs.

        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
  4. Import config from @vue/test-utils if you need to check calls to one of the global mocks defined in jest.setup.js

  5. Import any modules imported in the tested module that you will need to mock or spy on

  6. Start with an outer describe block naming the module or component being tested (i.e., describe('UserSingle component', () => {...});)

  7. Add any setup required for all tests of the module or component in this describe block

  8. Continue adding describe blocks and their relevant setup to create logical groupings for tests

  9. Add an it call, 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 expect call per it call helps maintain atomic tests
    • This ensures that, if a test fails, you don’t have to guess about what exactly needs to be fixed

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