typescript - چگونه ماژولهای Vuex که با نحو vuex-module-decorators در Nuxt تعریف شدهاند را با استفاده از vue-test-utils و Jest تست واحد کنیم؟
من نمیتوانم پاسخی برای این سؤال در هیچجا پیدا کنم.
من مستندات رسمی Nuxt و همچنین بحثهای موجود در Stack Overflow و مسائل Github را بررسی کردم.
پیادهسازی ماژول احراز هویت:
@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 تعریف شدهاند، با استفاده از Jest و vue-test-utils چیست؟
چگونه میتوانم VuexMutations، VuexActions، MutationActions، getters و غیره را به صورت واحد تست کنم؟
من سعی کردم کلاس 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
و اینجا:
آزمودن یک برنامه NUXT.js و Vue.js با Jest. دریافت خطاهای '[vuex] ماژول namespace در mapState() پیدا نشد' و '[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-decorators شروع کردهاید و در مواجهه با همین مشکل خاص گیر کردهاید، امیدوارم این پرسش و پاسخ کوچک تنهایی شما را خوب پیدا کند!
راهحل من شبیه این است:
// 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 ',
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 ';
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 فقط شیرینی نحوی هستند - یک لایه پوششی حول API vue-class-component.
برای نقل قول از مستندات:
در فروشگاه شما، خود کلاس MyModule را به عنوان یک ماژول استفاده میکنید... نحوهی استفاده ما از کلاس MyModule متفاوت از برنامهنویسی شیءگرا کلاسیک است و مشابه نحوه کار vue-class-component است. ما از خود کلاس به عنوان ماژول استفاده میکنیم، نه یک شیء ساخته شده توسط کلاس
چیز دیگری که باید به خاطر داشته باشید استفاده از createLocalVue است، که به ما امکان میدهد از کلاسها، پلاگینها، کامپوننتها و غیره Vue استفاده کنیم بدون اینکه کلاس جهانی Vue را آلوده کنیم.
اضافه کردن افزونه Vuex به createLocalVue:
localVue.use(Vuex);کلاس AuthModule به عنوان یک ماژول Vuex (با فضای نام) داخل سازنده Vuex.Store اعلام شده است (طبق مستندات).
const storeOptions = {
modules: {
auth: AuthModule,
},
};
const createStore = (storeOptions: any = {}): Store<{ auth: IState }> => new Vuex.Store({ ...storeOptions });
در پیادهسازی بالا، AuthModule (شامل store، actions، mutations، getters و غیره) برای هر تست با کمک هوک beforeEach دوباره ایجاد میشود (پس برای هر تست یک store تمیز داریم)
بقیه کار نسبتاً ساده است. میتوانید ببینید چگونه هر بخش از ماژول احراز هویت (عملیات، تغییرات، دریافتکنندهها..) را آزمایش کردم
