如何使用 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?
我找不到这个问题的答案。
我浏览了官方 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 我声明和实例化商店的地方。此外,模块不是动态的。
所以我的问题是:
为 vuex-module-decorators synax, using Jest and vue-test-utils 定义的 Nuxt Vuex 模块编写单元测试的正确模式是什么?
如何对 VuexMutations、VuexActions、MutationActions、getter 等进行单元测试?
我尝试在测试文件中实例化 AuthModule class,但我无法让它工作。
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
这里:
这是我根据这些文章/链接中的建议进行的设置:
// 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>/',
'^~/(.*)$': '<rootDir>/',
'^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-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 class-based module behaves just like a vue-class-component。
所有 vuex-module-decorators 的东西只是语法糖 - vue-class-component API.
的包装器
引用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
另一件要记住的事情是使用 createLocalVue,这使我们能够使用 Vue classes、插件、组件等,而不会污染全局 Vue class。
正在将 Vuex 插件添加到 createLocalVue
:
localVue.use(Vuex);
AuthModule class 在 Vuex.Store 构造函数中声明为 Vuex(命名空间)模块(根据文档)。
const storeOptions = {
modules: {
auth: AuthModule,
},
};
const createStore = (storeOptions: any = {}): Store<{ auth: IState }> => new Vuex.Store({ ...storeOptions });
在上面的实现中,在 beforeEach
钩子的帮助下为每个测试用例重新创建了 AuthModule(包括存储、动作、突变、getters...)(所以我们有一个干净的为每个测试存储)
剩下的就很简单了。您可以看到我是如何测试 AuthModule 的每个部分的(动作、突变、getters..)
我找不到这个问题的答案。
我浏览了官方 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 我声明和实例化商店的地方。此外,模块不是动态的。
所以我的问题是:
为 vuex-module-decorators synax, using Jest and vue-test-utils 定义的 Nuxt Vuex 模块编写单元测试的正确模式是什么?
如何对 VuexMutations、VuexActions、MutationActions、getter 等进行单元测试?
我尝试在测试文件中实例化 AuthModule class,但我无法让它工作。
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
这里:
这是我根据这些文章/链接中的建议进行的设置:
// 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>/',
'^~/(.*)$': '<rootDir>/',
'^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-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 class-based module behaves just like a vue-class-component。
所有 vuex-module-decorators 的东西只是语法糖 - vue-class-component API.
的包装器引用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
另一件要记住的事情是使用 createLocalVue,这使我们能够使用 Vue classes、插件、组件等,而不会污染全局 Vue class。
正在将 Vuex 插件添加到 createLocalVue
:
localVue.use(Vuex);
AuthModule class 在 Vuex.Store 构造函数中声明为 Vuex(命名空间)模块(根据文档)。
const storeOptions = {
modules: {
auth: AuthModule,
},
};
const createStore = (storeOptions: any = {}): Store<{ auth: IState }> => new Vuex.Store({ ...storeOptions });
在上面的实现中,在 beforeEach
钩子的帮助下为每个测试用例重新创建了 AuthModule(包括存储、动作、突变、getters...)(所以我们有一个干净的为每个测试存储)
剩下的就很简单了。您可以看到我是如何测试 AuthModule 的每个部分的(动作、突变、getters..)