DEV Community

Cover image for How to unit test Vuex modules defined with the vuex-module-decorators syntax in Nuxt, using vue-test-utils and Jest?
Ivan Spoljaric
Ivan Spoljaric

Posted on

 

How to unit test Vuex modules defined with the vuex-module-decorators syntax in Nuxt, using vue-test-utils and Jest?

Cover photo by Tien Vu Ngoc on Unsplash

The problem

To find the answer to my question I went through the official Nuxt documentation and through the existing Stack Overflow and Github issue discussions.

But with no luck.

My AuthModule looks something like this:

@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;
  }

  ... 

}
Enter fullscreen mode Exit fullscreen mode

The solution

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

The implementation 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: 'dummy@email.com',
        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 = 'dummy@email.com';

      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'));
  }
};

Enter fullscreen mode Exit fullscreen mode

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 class-based stuff coming from vuex-module-decorators 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);
Enter fullscreen mode Exit fullscreen mode

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 });
Enter fullscreen mode Exit fullscreen mode

In the implementation above, AuthModule (incl. store, actions, mutations, getters...) is re-created inside every test case with the help of the beforeEach hook (to have a clean store in every new iteration).

The rest is pretty straightforward.

Check out how I tested each part of the AuthModule (actions, mutations, getters..).

That's it. Happy unit testing :)

Top comments (1)

Collapse
 
championswimmer profile image
Arnav Gupta

The unit testing approach in my library itself should be a good starting point (yes it uses localVue.use only)

P.S. I am author of vuex-module-decorators

Visualizing Promises and Async/Await 🤓

async await

☝️ Check out this all-time classic DEV post on visualizing Promises and Async/Await 🤓