如何使用 vue-test-utils 和 Jest 在 Nuxt 中对使用 vuex-module-decorators 语法定义的 Vuex 模块进行单元测试?

Posted

技术标签:

【中文标题】如何使用 vue-test-utils 和 Jest 在 Nuxt 中对使用 vuex-module-decorators 语法定义的 Vuex 模块进行单元测试?【英文标题】:How to unit test Vuex modules defined with vuex-module-decorators syntax in Nuxt, using vue-test-utils and Jest? 【发布时间】:2021-11-17 17:50:54 【问题描述】:

我在任何地方都找不到这个问题的答案。

我浏览了 Nuxt 官方文档以及现有的 Stack Overflow 和 Github 问题讨论。

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;
  

  ... 


我的/store 目录仅包含 Vuex 模块(如示例 AuthModule)。我在其中声明和实例化存储的地方没有 index.ts。而且这些模块不是动态的。

所以我的问题是:

    为 Nuxt Vuex 模块编写单元测试的正确模式是什么,使用 vuex-module-decorators synax 定义,使用 Jest 和 vue-test-utils?

    如何对 VuexMutations、VuexActions、MutationActions、getter 等进行单元测试?

我尝试在测试文件中实例化 AuthModule 类,但我无法让它工作。

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: ''
    */
  );

我也尝试了这里解释的方法:

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

这里:

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

这是我根据这些文章/链接中的建议进行的设置:

// 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'],
  s-s-r: 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);
  );
);

运行此程序后,我在控制台中收到此错误:

 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'"

【问题讨论】:

【参考方案1】:

经过反复试验,我终于找到了问题的答案。

如果你和我一样;仅从 Vue、Nuxt 和 vuex-module-decorators 开始您的旅程,并且您在解决完全相同的问题时遇到了困难,我希望这个小小的单人 QA 乒乓球能找到您!

我的解决方案如下所示:

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

// 

要意识到的最重要的一点是,vuex-module-decorators 基于类的模块的行为就像引擎盖下的 vue-class-component。

所有 vuex-module-decorators 的东西都只是语法糖 - vue-class-component API 的包装器。

引用the docs:

在您的商店中,您将 MyModule 类本身用作模块...方式 我们使用的 MyModule 类不同于经典的面向对象 编程,类似于 vue-class-component 的工作方式。我们使用 类本身作为模块,而不是类构造的对象

要记住的另一件事是使用createLocalVue,它使我们能够在不污染全局Vue 类的情况下使用Vue 类、插件、组件等。

将 Vuex 插件添加到 createLocalVue:

localVue.use(Vuex);

AuthModule 类在 Vuex.Store 构造函数中声明为 Vuex(命名空间)模块(根据文档)。

const storeOptions = 
  modules: 
    auth: AuthModule,
  ,
;

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

在上面的实现中,在 beforeEach 钩子的帮助下,为每个测试用例重新创建了 AuthModule(包括存储、操作、突变、getter...)(因此我们为每个测试都有一个干净的存储)

剩下的就很简单了。你可以看到我是如何测试 AuthModule 的每个部分(动作、突变、getter..)

【讨论】:

以上是关于如何使用 vue-test-utils 和 Jest 在 Nuxt 中对使用 vuex-module-decorators 语法定义的 Vuex 模块进行单元测试?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 vue-test-utils 打开 bootstrap-vue 模态?

如何在单元测试期间使用带有 shallowMount 的 vue-test-utils 找到元素组件?

VueJs 如何删除全局错误处理程序以使用 vue-test-utils 进行测试

vue-test-utils:如何在 mount() 生命周期钩子(使用 vuex)中测试逻辑?

如何使用 vue-test-utils 和 Jest 在 Nuxt 中对使用 vuex-module-decorators 语法定义的 Vuex 模块进行单元测试?

如何使用 vue-test-utils 在单元测试期间触发窗口事件