Home > Back-end >  How to unit test Vuex modules defined with vuex-module-decorators syntax in Nuxt, using vue-test-uti
How to unit test Vuex modules defined with vuex-module-decorators syntax in Nuxt, using vue-test-uti

Time:09-29

I cannot find an answer to this question anywhere.

I went through the official Nuxt documentation and through the existing Stack Overflow and Github issue discussions.

Implementation of the AuthModule:

@Module({
  stateFactory: true,
  namespaced: true,
})
export default class AuthModule extends VuexModule {
  userData?: UserData | undefined = undefined;
  prevRouteList: Routes[] = [];
  error?: services.ICognitoError | undefined = undefined;
  isLoading = false;
  ...

  @VuexMutation
  setIsLoading(isLoading: boolean) {
    this.isLoading = isLoading;
  }
 
  ...

   @VuexAction({ rawError: true })
  async register(registerData: { email: string; password: string }): Promise<any> {
    this.context.commit('setIsLoading', true);
    this.context.commit('setError', undefined);
    this.context.commit('setInitiateRegistration', false);
    this.context.dispatch('setEmail', registerData.email);

    try {
      const { user } = await services.register(registerData.email, registerData.password);

      if (user) {
        this.context.dispatch('pushPrevRoute', Routes.emailVerification);
        this.context.commit('setInitiateRegistration', true);
      }
    } catch (error: any) {
      this.context.commit('setError', error);
      this.context.commit('setInitiateRegistration', false);
    }

    this.context.commit('setIsLoading', false);
  }

  ...

  @MutationAction
  setEmail(email: string)  { ... }

  ... 

  get getEmail() {
    return this.email;
  }

  ... 

}

My /store directory contains only Vuex modules (like the example AuthModule). There is no index.ts where I declare and instantiate the store. Also the modules are not dynamic.

So my questions are:

  1. What is the correct pattern of writing unit tests for Nuxt Vuex modules, defined with vuex-module-decorators synax, using Jest and vue-test-utils?

  2. How can I unit test VuexMutations, VuexActions, MutationActions, getters etc.?

I tried instantiating the AuthModule class inside the test file, but I can't get it to work.

describe('AuthModule', () => {
  const authModule = new AuthModule({...});

  it('test', () => {
   console.log(authModule);

   /* 
     AuthModule {
      actions: undefined,
      mutations: undefined,
      state: undefined,
      getters: undefined,
      namespaced: undefined,
      modules: undefined,
      userData: undefined,
      prevRouteList: [],
      error: undefined,
      isLoading: false,
      registrationInitiated: false,
      registrationConfirmed: false,
      forgotPasswordEmailSent: false,
      forgottenPasswordReset: false,
      email: '',
      maskedEmail: ''
    }*/
  });

I also tried the approach explained here:

https://medium.com/@brandonaaskov/how-to-test-nuxt-stores-with-jest-9a5d55d54b28

and here:

Testing a NUXT.js and Vue.js app with Jest. Getting '[vuex] module namespace not found in mapState()' and '[vuex] unknown action type'

Here's my setup based on the recommendations in those articles / links:

// jest.config.js

module.exports = {
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  roots: [
    '<rootDir>/components',
    '<rootDir>/pages',
    '<rootDir>/middleware',
    '<rootDir>/layouts',
    '<rootDir>/services',
    '<rootDir>/store',
    '<rootDir>/utils',
  ],
  reporters: ['default', 'jest-sonar'],
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/$1',
    '^~/(.*)$': '<rootDir>/$1',
    '^vue$': 'vue/dist/vue.common.js',
  },
  moduleFileExtensions: ['ts', 'js', 'vue', 'json'],
  testEnvironment: 'jsdom',
  transform: {
    '^. \\.ts$': 'ts-jest',
    '.*\\.(vue)$': 'vue-jest',
    '^. \\.(js|jsx)$': 'babel-jest-amcharts',
  },
  collectCoverage: true,
  collectCoverageFrom: [
    '<rootDir>/components/**/*.vue',
    '<rootDir>/pages/**/*.vue',
    '<rootDir>/layouts/**/*.vue',
    '<rootDir>/middleware/**/*.ts',
    '<rootDir>/store/**/*.ts',
    '<rootDir>/mixins/**/*.ts',
    '<rootDir>/services/**/*.ts',
  ],
  transformIgnorePatterns: ['[/\\\\]node_modules[/\\\\](?!(@amcharts)\\/). \\.(js|jsx|ts|tsx)$'],
  forceExit: !!process.env.CI,
};

// jest.setup.js

import { config } from '@vue/test-utils';
import { Nuxt, Builder } from 'nuxt';
import TsBuilder from '@nuxt/typescript-build';
import nuxtConfig from './nuxt.config';

config.stubs.nuxt = { template: '<div />' };
config.stubs['nuxt-link'] = { template: '<a><slot></slot></a>' };
config.mocks.$t = (msg) => msg;

const nuxtResetConfig = {
  loading: false,
  loadingIndicator: false,
  fetch: {
    client: false,
    server: false,
  },
  features: {
    store: true,
    layouts: false,
    meta: false,
    middleware: false,
    transitions: false,
    deprecations: false,
    validate: false,
    asyncData: false,
    fetch: false,
    clientOnline: false,
    clientPrefetch: false,
    clientUseUrl: false,
    componentAliases: false,
    componentClientOnly: false,
  },
  build: {
    indicator: false,
    terser: false,
  },
};

const nuxtBuildConfig = {
  ...nuxtConfig,
  ...nuxtResetConfig,
  dev: false,
  extensions: ['ts'],
  ssr: false,
  srcDir: nuxtConfig.srcDir,
  ignore: ['**/components/**/*', '**/layouts/**/*', '**/pages/**/*'],
};

const buildNuxt = async () => {
  const nuxt = new Nuxt(nuxtBuildConfig);
  await nuxt.moduleContainer.addModule(TsBuilder);

  try {
    await new Builder(nuxt).build();
    return nuxt;
  } catch (error) {
    console.log(error);
    process.exit(1);
  }
};

module.exports = async () => {
  const nuxt = await buildNuxt();
  process.env.buildDir = nuxt.options.buildDir;
};


// jest.utils.js

import Vuex from 'vuex';
import VueRouter from 'vue-router';
import VueFormulate from '@braid/vue-formulate';
import { mount, createLocalVue } from '@vue/test-utils';

const createStore = (storeOptions = {}) => new Vuex.Store({ ...storeOptions });
const createRouter = () => new VueRouter({});

const setup = (storeOptions) => {
  const localVue = createLocalVue();
  localVue.use(VueRouter);
  localVue.use(Vuex);
  localVue.use(VueFormulate);

  const store = createStore(storeOptions);
  const router = createRouter();
  return { store, router, localVue };
};

export const createNuxtStore = async () => {
  const storePath = `${process.env.buildDir}/store.js`;

  // console.log(storePath);
  const NuxtStoreFactory = await import(storePath);
  const nuxtStore = await NuxtStoreFactory.createStore();

  return { nuxtStore };
};

export const createTestBed =
  (component, componentOptions = {}, storeOptions = {}) =>
  (renderer = mount) => {
    const { localVue, store, router } = setup(storeOptions);

    return renderer(component, {
      store,
      router,
      localVue,
      ...componentOptions,
    });
  };

// auth.spec.js

import { createNuxtStore } from '@/jest.utils';

describe('AuthModule', () => {
  let store: any;

  beforeAll(() => {
    store = createNuxtStore();
  });

  it('should create', () => {
    console.log(store);
  });
});

After I run this I get this error in the console:

 RUNS  store/auth.spec.ts
node:internal/process/promises:245
          triggerUncaughtException(err, true /* fromPromise */);
          ^

ModuleNotFoundError: Cannot find module 'undefined/store.js' from 'jest.utils.js'
    at Resolver.resolveModule (/Users/ivan.spoljaric/Documents/.../node_modules/jest-resolve/build/index.js:306:11)
    at Resolver._getVirtualMockPath (/Users/ivan.spoljaric/Documents/.../node_modules/jest-resolve/build/index.js:445:14)
    at Resolver._getAbsolutePath (/Users/ivan.spoljaric/Documents/.../node_modules/jest-resolve/build/index.js:431:14)
    at Resolver.getModuleID (/Users/ivan.spoljaric/Documents/.../node_modules/jest-resolve/build/index.js:404:31)
    at Runtime._shouldMock (/Users/ivan.spoljaric/Documents/.../node_modules/jest-runtime/build/index.js:1521:37)
    at Runtime.requireModuleOrMock (/Users/ivan.spoljaric/Documents/.../node_modules/jest-runtime/build/index.js:916:16)
    at /Users/ivan.spoljaric/Documents/.../jest.utils.js:24:28
    at processTicksAndRejections (node:internal/process/task_queues:94:5)
    at Object.createNuxtStore (/Users/ivan.spoljaric/Documents/.../jest.utils.js:24:28) {
  code: 'MODULE_NOT_FOUND',
  hint: '',
  requireStack: undefined,
  siblingWithSimilarExtensionFound: false,
  moduleName: 'undefined/store.js',
  _originalMessage: "Cannot find module 'undefined/store.js' from 'jest.utils.js'"

CodePudding user response:

After some trial and error I finally discovered the answer to my question.

If you are like me; only starting your journey with Vue, Nuxt & vuex-module-decorators and you get stuck tackling this exact same problem, I hope this little solo QA ping-pong finds you well!

My solution looks like this:

// auth.spec.ts

import Vuex, { Store } from 'vuex';
import { createLocalVue } from '@vue/test-utils';

import AuthModule, { IState } from './auth';

jest.mock('@/services');

const localVue = createLocalVue();
localVue.use(Vuex);

const storeOptions = {
  modules: {
    auth: AuthModule,
  },
};

const createStore = (storeOptions: any = {}): Store<{ auth: IState }> => new Vuex.Store({ ...storeOptions });

describe('AuthModule', () => {
  let store: Store<{ auth: IState }>;

  beforeEach(() => {
    store = createStore(storeOptions);
  });

  describe('mutations', () => {
    // ...

    it('auth/setIsLoading', () => {
      expect(store.state.auth.isLoading).toBe(false);
      store.commit('auth/setIsLoading', true);
      expect(store.state.auth.isLoading).toBe(true);
    });

    // ...
  });

  describe('actions', () => {
    // ...

    it('register success', async () => {
      const registerData = {
        email: '[email protected]',
        password: 'dummy',
      };

      expect(store.state.auth.registrationInitiated).toBe(false);

      try {
        await store.dispatch('auth/register', registerData);
        expect(store.state.auth.registrationInitiated).toBe(true);
      } catch (error) {}
    });

    // ...
  });

  describe('mutation-actions', () => {
    // ...

    it('setEmail', async () => {
      const dummyEmail = '[email protected]';

      expect(store.state.auth.email).toBe('');
      await store.dispatch('auth/setEmail', dummyEmail);
      expect(store.state.auth.email).toBe(dummyEmail);
    });

    // ...
  });

  describe('getters', () => {
    // ...

    it('auth/getError', () => {
      expect(store.state.auth.error).toBe(undefined);
      expect(store.getters['auth/getError']).toBe(undefined);

      (store.state.auth.error as any) = 'Demmo error';
      expect(store.getters['auth/getError']).toBe('Demmo error');
    });

    // ...
  });
});

// services/auth

export async function register(email: string, password: string, attr: any = {}): Promise<any> {
  try {
    return await Auth.signUp({
      username: email,
      password,
      attributes: {
        ...attr,
      },
    });
  } catch (err: any) {
    return Promise.reject(createError(err, 'register'));
  }
}

// createError is just a util method for formatting the error message and wiring to the correct i18n label

// services/__mock__/auth

import { createError } from '../auth';

export const register = (registerData: { email: string; password: string }) => {
  try {
    if (!registerData) {
      throw new Error('dummy error');
    }

    return new Promise((resolve) => resolve({ response: { user: registerData.email } }));
  } catch (err) {
    return Promise.reject(createError(err, 'register'));
  }
};

// 

The most important thing to realise is that the vuex-module-decorators class-based module behaves just like a vue-class-component under the hood.

All of the vuex-module-decorators stuff is just syntactic sugar - a wrapper around the vue-class-component API.

To quote the docs:

In your store, you use the MyModule class itself as a module...The way we use the MyModule class is different from classical object-oriented programming and similar to how vue-class-component works. We use the class itself as module, not an object constructed by the class

Another thing to keep in mind is to use createLocalVue, which enables us to use Vue classes, plugins, components etc. without polluting the global Vue class.

Adding the Vuex plugin to createLocalVue:

localVue.use(Vuex);

The AuthModule class is declared as a Vuex (namespaced) module inside the Vuex.Store constructor (as per docs).

const storeOptions = {
  modules: {
    auth: AuthModule,
  },
};

const createStore = (storeOptions: any = {}): Store<{ auth: IState }> => new Vuex.Store({ ...storeOptions });

In the implementation above, AuthModule (incl. store, actions, mutations, getters...) is re-created for every test case with the help of the beforeEach hook (so we have a clean store for every test)

The rest is pretty straightforward. You can see how I tested each part of the AuthModule (actions, mutations, getters..)

  • Related