react-native-web(ts) + jest + path alias 적용
2021. 11. 5. 17:39ㆍ공부내용 공유하기
jest를 사용한 테스트 코드는 생각보다 쉽고 재밌었는데, 환경 구축이 엄청 빡셌다 ㅠ
path alias 적용
1.
tsconfig.json에 절대경로 추가
"baseUrl": ".",
"paths": {
"@src/*": ["src/*"]
},
2.
tsconfig-paths 설치
babel-plugin-module-resolver 설치
3.
babel.config.js - plugins에 module-resolver 설정 추가
[
"module-resolver",
{
root: ["."],
extensions: [".ts", ".tsx", ".jsx", ".js", ".json"],
alias: {
"@src": "./src",
},
},
],
4.
webpack.config.js 내 alias 추가
"@src": path.resolve(__dirname, "src"),
jest 세팅
1.
jest는 원래 설치되어 있었음. 추가적으로 사용하기 위해 아래 서드파티 설치
jest-environment-node
jest-environment-jsdom
jsdom
jest-enzyme
enzyme
enzyme-adapter-react-16
enzyme-to-json
설치
2.
setup-tests.js 추가
// setup-tests.js
import "react-native";
import "jest-enzyme";
import Adapter from "enzyme-adapter-react-16";
import Enzyme from "enzyme";
import { TextEncoder, TextDecoder } from "util";
global.TextEncoder = TextEncoder;
global.TextDecoder = TextDecoder;
/**
* Set up DOM in node.js environment for Enzyme to mount to
*/
const { JSDOM } = require("jsdom");
const jsdom = new JSDOM("<!doctype html><html><body></body></html>");
const { window } = jsdom;
function copyProps(src, target) {
Object.defineProperties(target, {
...Object.getOwnPropertyDescriptors(src),
...Object.getOwnPropertyDescriptors(target),
});
}
global.window = window;
global.document = window.document;
global.navigator = {
userAgent: "node.js",
};
copyProps(window, global);
/**
* Set up Enzyme to mount to DOM, simulate events,
* and inspect the DOM in tests.
*/
Enzyme.configure({ adapter: new Adapter() });
3.
jest.config.js 추가 (기존 package.json 설정도 이관)
const { pathsToModuleNameMapper } = require("ts-jest/utils");
const { compilerOptions } = require("./tsconfig");
module.exports = {
preset: "ts-jest",
globals: {
__DEV__: true,
},
collectCoverageFrom: ["src/**/*.{js,jsx,ts,tsx}", "!src/**/*.d.ts"],
setupFiles: ["react-app-polyfill/jsdom", "<rootDir>/src/testHelper/setup.js"],
setupFilesAfterEnv: ["<rootDir>/src/testHelper/setup-tests.js"],
snapshotSerializers: ["enzyme-to-json/serializer"],
testMatch: [
"<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
"<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}",
],
testEnvironment: "jsdom",
testRunner: "<rootDir>/node_modules/jest-circus/runner.js",
transform: {
"^.+\\.(js|jsx|mjs|cjs|ts|tsx)$": "<rootDir>/config/jest/babelTransform.js",
"^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
"^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)":
"<rootDir>/config/jest/fileTransform.js",
},
transformIgnorePatterns: ["^.+\\.module\\.(css|sass|scss)$"],
moduleFileExtensions: [
"web.js",
"js",
"web.ts",
"ts",
"web.tsx",
"tsx",
"json",
"web.jsx",
"jsx",
"node",
],
watchPlugins: [
"jest-watch-typeahead/filename",
"jest-watch-typeahead/testname",
],
resetMocks: true,
roots: ["<rootDir>"],
modulePaths: ["<rootDir>"],
moduleDirectories: [".", "src", "node_modules"],
moduleNameMapper: {
"^react-native$": "react-native-web",
"^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy",
...pathsToModuleNameMapper(compilerOptions.paths),
},
};
jest 테스트 util 추가
1.
getters.ts
import { ReactWrapper } from "enzyme";
const propSelector = (
propsKey: string | string[],
value: string
): ((node: ReactWrapper<any, any>) => boolean) => {
return (node: ReactWrapper<any, any>) => {
if (typeof propsKey === "string") {
return node.prop(propsKey) === value;
} else {
return !!propsKey.find((key) => node.prop(key) === value);
}
};
};
export function innerTextSelector(
text: string
): (node: ReactWrapper<any, any>) => boolean {
return (node: ReactWrapper<any, any>) => {
return node.text() === text;
};
}
export function getByBtnText(node: ReactWrapper<any, any>, buttonText: string) {
return node.findWhere(propSelector(["label", "title"], buttonText)).first();
}
export function getByChildText(
node: ReactWrapper<any, any>,
childText: string
) {
return node.findWhere(innerTextSelector(childText)).first();
}
export function getElementByText(node: ReactWrapper<any, any>, text: string) {
const btnText = getByBtnText(node, text);
if (btnText.exists()) {
return btnText;
}
const byChild = getByChildText(node, text);
return byChild;
}
export function getElementById(node: ReactWrapper<any, any>, testId: string) {
return node.findWhere(propSelector("testID", testId)).first();
}
export function getElementByName(node: ReactWrapper<any, any>, name: string) {
return node.findWhere(propSelector("name", name)).first();
}
2.
waitForComponentToPaint.ts 추가 -> act로 액션을 래핑 후 업데이트를 잠깐 기다려줘야 정상 반영된다고 한다.
// eslint-disable-next-line @typescript-eslint/ban-types
import { ReactWrapper } from "enzyme";
import React from "react";
import { act } from "react-dom/test-utils";
export type TestWrapper = ReactWrapper<any, any, React.Component<{}, {}, any>>;
export const waitForComponentToPaint = async (wrapper, action) => {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
await act(async () => {
action();
await new Promise((resolve) =>
setTimeout(() => {
resolve(undefined);
}, 10)
);
wrapper.update();
});
};
3.
테스트 코드 작성
import React from "react";
import { AuthFlow } from "@src/navigation";
import makeFlow from "../../testHelper/makeFlow";
import { mount } from "enzyme";
import { getElementById, getElementByText } from "@src/testHelper/testGetter";
import {
TestWrapper,
waitForComponentToPaint,
} from "@src/testHelper/waitForComponentToPaint";
import { ScreenName } from "@src/types/screen";
let props;
let loginScreen: TestWrapper;
let inputId: TestWrapper;
let inputPw: TestWrapper;
let submitBtn: TestWrapper;
let signupBtn: TestWrapper;
let findPwBtn: TestWrapper;
describe("로그인 플로우 테스트", () => {
props = {};
const flow = makeFlow(<AuthFlow />);
const authFlow = mount(flow);
const updateLoginScreen = () => {
loginScreen = getElementById(authFlow, "login_screen");
inputId = getElementById(loginScreen, "input_id");
inputPw = getElementById(loginScreen, "input_pw");
submitBtn = getElementById(loginScreen, "login_submit");
signupBtn = getElementByText(loginScreen, "회원가입");
findPwBtn = getElementByText(loginScreen, "아이디/비밀번호 찾기");
};
updateLoginScreen();
const spy = jest.spyOn(loginScreen.props().navigation, "navigate");
const updateWithAction = async (action) => {
await waitForComponentToPaint(authFlow, () => {
action();
});
updateLoginScreen();
};
it("컴포넌트 정상 마운트 확인", () => {
expect(loginScreen).toExist();
expect(inputId).toExist();
expect(inputPw).toExist();
expect(submitBtn).toExist();
expect(signupBtn).toExist();
expect(inputId.props().value).toBe("");
expect(inputPw.props().value).toBe("");
});
it("로그인 실패", async () => {
await updateWithAction(() => {
submitBtn.props().onPress();
});
expect(spy).not.toBeCalled();
});
it("아이디 비밀번호 입력", async () => {
await updateWithAction(() => {
inputId.props().onChangeText("slogupid");
inputPw.props().onChangeText("123123123q");
});
expect(inputId.props().value).toBe("slogupid");
expect(inputPw.props().value).toBe("123123123q");
});
it("로그인 성공 -> 홈 화면 이동", async () => {
expect(submitBtn).toExist();
await updateWithAction(() => {
submitBtn.props().onPress();
});
expect(spy).toBeCalledWith(ScreenName.HOME);
});
it("회원가입 버튼 -> 회원가입 이동", async () => {
expect(signupBtn).toExist();
await updateWithAction(() => {
signupBtn.props().onPress();
});
expect(spy).toBeCalledWith(ScreenName.SIGN_UP);
});
it("아이디/비밀번호 찾기 버튼 -> 본인인증 이동", async () => {
expect(findPwBtn).toExist();
await updateWithAction(() => {
findPwBtn.props().onPress();
});
expect(spy).toBeCalled();
});
});