Programing

Nuxt의 Vuex-module-decorator 구문으로 정의된 Vuex 모듈을 vue-test-utils 및 Jest를 사용하여 테스트하는 방법

c10106 2022. 3. 17. 20:50
반응형

Nuxt의 Vuex-module-decorator 구문으로 정의된 Vuex 모듈을 vue-test-utils 및 Jest를 사용하여 테스트하는 방법

나는 이 질문에 대한 답을 어디서도 찾을 수 없다.

공식 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).내가 매장을 신고하고 인스턴스화하는 곳에는 인덱스가 없다.또한 그 모듈들은 역동적이지 않다.

그래서 제 질문은:

  1. Jestvue-test-utils를 사용하여 vuex-module-decorators synax로 정의된 Nuxt Vuex 모듈의 단위 테스트의 올바른 패턴은 무엇인가?

  2. VuexMutions, VuexActions, 돌연변이Actions, 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/테스트 방법-nuxt-store-with-jest-9a5d55d54b28

다음 항목:

Jest와 함께 NUXT.js 및 Vue.js 앱 테스트.mapState()에서 '[vuex] 모듈 네임스페이스를 찾을 수 없음' 및 '[vuex] 알 수 없는 작업 유형'을 가져오는 중

다음은 해당 기사/링크의 권장 사항을 기반으로 한 설정:

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

이 작업을 실행한 후 콘솔에 다음 오류가 표시됨:

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

약간의 시행착오 끝에 나는 마침내 내 질문에 대한 답을 찾았다.

만약 당신이 나와 같다면, 단지 Vue, Nuxt & Vuex-module-deodule-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-모듈 디코레이터 물질은 그저 통사성 설탕일 뿐이며, vue-class-component API를 감싸고 있다.

문서를 인용하려면:

당신의 가게에서는 마이모듈 클래스 자체를 모듈로 사용하고...우리가 MyModule 클래스를 사용하는 방식은 고전적인 객체 지향 프로그래밍과는 다르며, vue-class-component가 작동하는 방식과 유사하다.우리는 클래스 자체를 모듈로 사용하는 것이지 클래스에 의해 구성된 객체가 아니다.

글로벌 Vue 클래스를 오염시키지 않고 Vue 클래스, 플러그인, 구성 요소 등을 사용할 수 있는 createLocalVue를 사용하는 것도 염두에 둬야 한다.

그 당시 인데 추가createLocalVue:

localVue.use(Vuex);

AuthModule 클래스는 Vuex 내에서 Vuex(이름 우선) 모듈로 선언된다.생성자 저장(문서별)

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

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

《기》의을 다시 한다.beforeEach훅(그래서 우리는 모든 시험을 위한 깨끗한 가게를 가지고 있다)

나머지는 꽤 간단하다.내가 AuthModule의 각 부분(액션, 돌연변이, 게터 등)을 어떻게 테스트했는지 알 수 있다.

참조URL: https://stackoverflow.com/questions/69315107/how-to-unit-test-vuex-modules-defined-with-vuex-module-decorators-syntax-in-nuxt

반응형