@dannysir/js-te 0.0.2 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,10 +27,12 @@ describe('[단순 연산 테스트]', () => {
27
27
 
28
28
  ### 2. 테스트 실행
29
29
 
30
- package.json에 추가:
30
+ package.json에 추가.
31
31
 
32
+ - type을 module로 설정해주세요.
32
33
  ```json
33
34
  {
35
+ "type": "module",
34
36
  "scripts": {
35
37
  "test": "js-te"
36
38
  }
@@ -127,41 +129,40 @@ Babel을 사용해서 import 구문을 변환하여 mock 함수를 가져오도
127
129
  2. `path`를 key로 이용해 Map에 저장
128
130
  2. Babel로 코드 변환
129
131
  1. 전체 파일의 import문 확인
130
- 2. import 경로가 Map에 존재하면 mock 객체로 변환
131
- 3. import 경로가 Map에 없다면 그대로 import
132
+ 2. (0.0.3 버전 추가) import 경로를 **절대 경로**로 변환
133
+ 2. import 경로(절대 경로)가 Map에 존재하면 mock 객체로 변환
134
+ 3. import 경로(절대 경로)가 Map에 없다면 그대로 import
132
135
  3. 테스트 실행
133
136
  4. 원본 파일 복구
134
137
 
135
- ### 🚨 주의 사항 (현재 수정 )
138
+ ### `mock(모듈 절대 경로), mock객체)`
136
139
 
137
- mocking 기능의 경우 현재 `path`를 기반으로 직접 변환을 하기 때문에 mocking이 필요한 함수의 경우
138
-
139
- 반드시 **절대 경로**로 표현한 후 `mock` 함수에 **절대 경로**로 등록을 해주세요.
140
-
141
- > 만약 모듈이 사용되는 모든 위치의 path가 동일하다면 상대 경로도 정삭 작동합니다.
140
+ 모듈을 모킹합니다. import 하기 **전에** 호출해야 합니다.
142
141
 
143
- ### `mock(모듈경로, mock객체)`
142
+ **🚨 주의사항**
144
143
 
145
- 모듈을 모킹합니다. import 하기 **전에** 호출해야 합니다.
144
+ 1. 반드시 경로는 절대 경로로 입력해주세요.
145
+ - babel이 import문에서 절대 경로로 변환하여 확인을 하기 때문에 반드시 절대 경로로 등록해주세요.
146
+ 2. import문을 반드시 mocking 이후에 선언해주세요.
147
+ - mocking 전에 import를 하게 되면 mocking되기 전의 모듈을 가져오게 됩니다.
146
148
 
147
149
  ```javascript
148
150
  // random.js
149
151
  export const random = () => Math.random();
150
152
 
151
153
  // game.js
152
- import { random } from './random.js';
154
+ import { random } from './random.js'; // 자유롭게 import하면 babel에서 절대 경로로 변환하여 판단합니다.
153
155
  export const play = () => random() * 10;
154
156
 
155
157
  // game.test.js
156
- import { mock, test, expect } from 'js-te';
157
-
158
158
  test('랜덤 함수 모킹', async () => {
159
159
  // 1. 먼저 모킹
160
- mock('./random.js', {
160
+ mock('/Users/san/Js-Te/test-helper/random.js', { // 반드시 절대 경로로 등록
161
161
  random: () => 0.5
162
162
  });
163
163
 
164
164
  // 2. 그 다음 import
165
+ // 상단에 import문을 입력할 경우
165
166
  const { play } = await import('./game.js');
166
167
 
167
168
  // 3. 모킹된 값 사용
@@ -181,6 +182,82 @@ test('랜덤 함수 모킹', async () => {
181
182
 
182
183
  mock이 등록되어 있는지 확인합니다.
183
184
 
185
+ ## `each(cases)`
186
+
187
+ `cases`를 배열로 받아 순차적으로 테스트 진행
188
+
189
+ #### 🚨 주의 사항
190
+
191
+ `cases`는 반드시 `Array` 타입으로 받아야 합니다.
192
+
193
+ ### 플레이스 홀더
194
+
195
+ - %s - 문자열/숫자
196
+ - %o - 객체 (JSON.stringify)
197
+
198
+ ```jsx
199
+ test.each([
200
+ [1, 2, 3, 6],
201
+ [3, 4, 5, 12],
202
+ [10, 20, 13, 43],
203
+ [10, 12, 13, 35],
204
+ ])('[each test] - input : %s, %s, %s, %s', (a, b, c, result) => {
205
+ expect(a + b + c).toBe(result);
206
+ });
207
+
208
+ /* 출력 결과
209
+ ✓ [each test] - input : 1, 2, 3, 6
210
+ ✓ [each test] - input : 3, 4, 5, 12
211
+ ✓ [each test] - input : 10, 20, 13, 43
212
+ ✓ [each test] - input : 10, 12, 13, 35
213
+ */
214
+
215
+ test.each([
216
+ [{ name : 'dannysir', age : null}],
217
+ ])('[each test placeholder] - input : %o', (arg) => {
218
+ expect(arg.name).toBe('dannysir');
219
+ });
220
+
221
+ /* 출력 결과
222
+ ✓ [each test placeholder] - input : {"name":"dannysir","age":null}
223
+ */
224
+ ```
225
+
226
+ ## `beforeEach(함수)`
227
+
228
+ 각 테스트가 진행되기 전에 실행할 함수를 선언합니다.
229
+
230
+ 중첩된 describe에서의 `beforeEach`는 상위 describe의 `beforeEach`를 모두 실행한 후, 자신의 `beforeEach`를 실행합니다.
231
+
232
+ ```jsx
233
+ describe('카운터 테스트', () => {
234
+ let counter;
235
+
236
+ beforeEach(() => {
237
+ counter = 0;
238
+ });
239
+
240
+ test('카운터 증가', () => {
241
+ counter++;
242
+ expect(counter).toBe(1);
243
+ });
244
+
245
+ test('카운터는 0부터 시작', () => {
246
+ expect(counter).toBe(0);
247
+ });
248
+
249
+ describe('중첩된 describe', () => {
250
+ beforeEach(() => {
251
+ counter = 10;
252
+ });
253
+
254
+ test('카운터는 10', () => {
255
+ expect(counter).toBe(10);
256
+ });
257
+ });
258
+ });
259
+ ```
260
+
184
261
  ## 테스트 파일 찾기 규칙
185
262
 
186
263
  자동으로 다음 파일들을 찾아서 실행합니다:
@@ -204,8 +281,6 @@ mock이 등록되어 있는지 확인합니다.
204
281
  ### 기본 테스트
205
282
 
206
283
  ```javascript
207
- import { describe, test, expect } from 'js-te';
208
-
209
284
  describe('문자열 테스트', () => {
210
285
  test('문자열 합치기', () => {
211
286
  const result = 'hello' + ' ' + 'world';
@@ -222,10 +297,8 @@ describe('문자열 테스트', () => {
222
297
 
223
298
  ```javascript
224
299
  // mocking.test.js
225
- import { mock, test, expect } from 'js-te';
226
-
227
300
  test('[mocking] - mocking random function', async () => {
228
- mock('/src/test-helper/random.js', {
301
+ mock('/Users/san/Js-Te/test-helper/random.js', {
229
302
  random: () => 3,
230
303
  });
231
304
  const {play} = await import('../src/test-helper/game.js');
@@ -234,7 +307,7 @@ test('[mocking] - mocking random function', async () => {
234
307
 
235
308
 
236
309
  // game.js
237
- import {random} from '/src/test-helper/random.js'
310
+ import {random} from '/test-helper/random.js'
238
311
 
239
312
  export const play = () => {
240
313
  return random() * 10;
@@ -244,16 +317,6 @@ export const play = () => {
244
317
  export const random = () => Math.random();
245
318
  ```
246
319
 
247
- ## 설정
248
-
249
- `package.json`에 해당 설정을 하셔야 정상 작동합니다.
250
-
251
- ```json
252
- {
253
- "type": "module"
254
- }
255
- ```
256
-
257
320
  ## 링크
258
321
 
259
322
  - [GitHub](https://github.com/dannysir/Js-Te)
@@ -1,44 +1,53 @@
1
+ import path from 'path';
2
+ import {BABEL, MOCK} from "./constants.js";
3
+
1
4
  export const babelTransformImport = ({types: t}) => {
2
5
  return {
3
6
  visitor: {
4
7
  Program(path) {
5
8
  const importStatement = t.ImportDeclaration(
6
9
  [t.importSpecifier(
7
- t.identifier('__mockRegistry__'),
8
- t.identifier('__mockRegistry__')
10
+ t.identifier(MOCK.STORE_NAME),
11
+ t.identifier(MOCK.STORE_NAME)
9
12
  )],
10
- t.stringLiteral('@dannysir/js-te/src/mock/store.js')
13
+ t.stringLiteral(MOCK.STORE_PATH)
11
14
  );
12
15
  path.node.body.unshift(importStatement);
13
16
  },
14
17
 
15
- ImportDeclaration(path) {
16
- const source = path.node.source.value;
18
+ ImportDeclaration(nodePath, state) {
19
+ const source = nodePath.node.source.value;
17
20
 
18
- if (source === '@dannysir/js-te/src/mock/store.js') {
21
+ if (source === MOCK.STORE_PATH) {
19
22
  return;
20
23
  }
21
24
 
22
- const specifiers = path.node.specifiers;
25
+ const currentFilePath = state.filename || process.cwd();
26
+ const currentDir = path.dirname(currentFilePath);
27
+
28
+ let absolutePath;
29
+ if (source.startsWith(BABEL.PERIOD)) {
30
+ absolutePath = path.resolve(currentDir, source);
31
+ } else {
32
+ absolutePath = source;
33
+ }
34
+
35
+ const specifiers = nodePath.node.specifiers;
23
36
 
24
- // 1. 먼저 모듈을 한 번만 가져오기 (mock이면 mock, 아니면 실제)
25
- const moduleVarName = path.scope.generateUidIdentifier('module');
37
+ const moduleVarName = nodePath.scope.generateUidIdentifier(BABEL.MODULE);
26
38
 
27
- const moduleDeclaration = t.variableDeclaration('const', [
39
+ const moduleDeclaration = t.variableDeclaration(BABEL.CONST, [
28
40
  t.variableDeclarator(
29
41
  moduleVarName,
30
42
  t.conditionalExpression(
31
- // 조건: __mockRegistry__.has(source)
32
43
  t.callExpression(
33
- t.memberExpression(t.identifier('__mockRegistry__'), t.identifier('has')),
34
- [t.stringLiteral(source)]
44
+ t.memberExpression(t.identifier(MOCK.STORE_NAME), t.identifier(BABEL.HAS)),
45
+ [t.stringLiteral(absolutePath)]
35
46
  ),
36
- // true: mock 반환
37
47
  t.callExpression(
38
- t.memberExpression(t.identifier('__mockRegistry__'), t.identifier('get')),
39
- [t.stringLiteral(source)]
48
+ t.memberExpression(t.identifier(MOCK.STORE_NAME), t.identifier(BABEL.GET)),
49
+ [t.stringLiteral(absolutePath)]
40
50
  ),
41
- // false: 실제 import
42
51
  t.awaitExpression(
43
52
  t.importExpression(t.stringLiteral(source))
44
53
  )
@@ -46,7 +55,6 @@ export const babelTransformImport = ({types: t}) => {
46
55
  )
47
56
  ]);
48
57
 
49
- // 2. 각 specifier를 moduleVarName에서 추출
50
58
  const extractDeclarations = specifiers.map(spec => {
51
59
  let importedName, localName;
52
60
 
@@ -55,7 +63,6 @@ export const babelTransformImport = ({types: t}) => {
55
63
  localName = spec.local.name;
56
64
  } else if (t.isImportNamespaceSpecifier(spec)) {
57
65
  localName = spec.local.name;
58
- // namespace import는 전체 모듈
59
66
  return t.variableDeclarator(
60
67
  t.identifier(localName),
61
68
  moduleVarName
@@ -71,9 +78,9 @@ export const babelTransformImport = ({types: t}) => {
71
78
  );
72
79
  });
73
80
 
74
- const extractDeclaration = t.variableDeclaration('const', extractDeclarations);
81
+ const extractDeclaration = t.variableDeclaration(BABEL.CONST, extractDeclarations);
75
82
 
76
- path.replaceWithMultiple([moduleDeclaration, extractDeclaration]);
83
+ nodePath.replaceWithMultiple([moduleDeclaration, extractDeclaration]);
77
84
  }
78
85
  }
79
86
  };
package/bin/cli.js CHANGED
@@ -6,7 +6,7 @@ import {transformSync} from '@babel/core';
6
6
  import * as jsTe from '../index.js';
7
7
  import {green, yellow} from "../utils/consoleColor.js";
8
8
  import {getTestResultMsg} from "../utils/makeMessage.js";
9
- import {RESULT_TITLE} from "../constants.js";
9
+ import {BABEL, PATH, RESULT_TITLE} from "../constants.js";
10
10
  import { babelTransformImport } from '../babelTransformImport.js';
11
11
 
12
12
  let totalPassed = 0;
@@ -25,7 +25,7 @@ const transformFile = (filePath) => {
25
25
  filename: filePath,
26
26
  plugins: [babelTransformImport],
27
27
  parserOpts: {
28
- sourceType: 'module',
28
+ sourceType: BABEL.MODULE,
29
29
  plugins: ['dynamicImport']
30
30
  }
31
31
  });
@@ -46,18 +46,18 @@ const findTestFiles = (dir) => {
46
46
  const items = fs.readdirSync(directory);
47
47
  const dirName = path.basename(directory);
48
48
 
49
- const isTestDir = dirName === 'test' || inTestDir;
49
+ const isTestDir = dirName === PATH.TEST_DIRECTORY || inTestDir;
50
50
 
51
51
  for (const item of items) {
52
- if (item === 'node_modules') continue;
52
+ if (item === PATH.NODE_MODULES) continue;
53
53
 
54
54
  const fullPath = path.join(directory, item);
55
55
  const stat = fs.statSync(fullPath);
56
56
 
57
57
  if (stat.isDirectory()) {
58
58
  walk(fullPath, isTestDir);
59
- } else if (item.endsWith('.test.js') || isTestDir) {
60
- if (item.endsWith('.js')) {
59
+ } else if (item.endsWith(PATH.TEST_FILE) || isTestDir) {
60
+ if (item.endsWith(PATH.JAVASCRIPT_FILE)) {
61
61
  files.push(fullPath);
62
62
  }
63
63
  }
@@ -75,14 +75,14 @@ const findAllSourceFiles = (dir) => {
75
75
  const items = fs.readdirSync(directory);
76
76
 
77
77
  for (const item of items) {
78
- if (item === 'node_modules' || item === 'bin' || item === 'test') continue;
78
+ if (item === PATH.NODE_MODULES || item === PATH.BIN || item === PATH.TEST_DIRECTORY) continue;
79
79
 
80
80
  const fullPath = path.join(directory, item);
81
81
  const stat = fs.statSync(fullPath);
82
82
 
83
83
  if (stat.isDirectory()) {
84
84
  walk(fullPath);
85
- } else if (item.endsWith('.js') && !item.endsWith('.test.js')) {
85
+ } else if (item.endsWith(PATH.JAVASCRIPT_FILE) && !item.endsWith(PATH.TEST_FILE)) {
86
86
  files.push(fullPath);
87
87
  }
88
88
  }
package/constants.js CHANGED
@@ -25,4 +25,26 @@ export const COLORS = {
25
25
  cyan: '\x1b[36m',
26
26
  gray: '\x1b[90m',
27
27
  bold: '\x1b[1m'
28
+ };
29
+
30
+ export const MOCK = {
31
+ STORE_NAME: 'mockStore',
32
+ STORE_PATH : '@dannysir/js-te/src/mock/store.js'
33
+ };
34
+
35
+ export const BABEL = {
36
+ MODULE: 'module',
37
+ CONST: 'const',
38
+ HAS: 'has',
39
+ GET: 'get',
40
+ PERIOD: '.',
41
+ };
42
+
43
+ export const PATH = {
44
+ NODE_MODULES: 'node_modules',
45
+ TEST_DIRECTORY: 'test',
46
+ TEST_FILE: '.test.js',
47
+ JAVASCRIPT_FILE: '.js',
48
+ BIN: 'bin',
49
+
28
50
  };
package/index.js CHANGED
@@ -6,15 +6,19 @@ import {
6
6
  import {Tests} from "./src/tests.js";
7
7
  import {green, red} from "./utils/consoleColor.js";
8
8
  import {getTestResultMsg} from "./utils/makeMessage.js";
9
+ import {clearAllMocks} from "./src/mock/store.js";
9
10
 
10
11
  const tests = new Tests();
11
12
 
12
13
  export { mock, clearAllMocks, unmock, isMocked } from './src/mock/store.js';
13
14
 
14
15
  export const test = (description, fn) => tests.test(description, fn);
16
+ test.each = (cases) => tests.testEach(cases);
15
17
 
16
18
  export const describe = (suiteName, fn) => tests.describe(suiteName, fn);
17
19
 
20
+ export const beforeEach = (fn) => tests.beforeEach(fn);
21
+
18
22
  export const expect = (actual) => {
19
23
  let value = actual;
20
24
 
@@ -73,6 +77,7 @@ export const run = async () => {
73
77
  const directoryString = green(CHECK) + (test.path === '' ? EMPTY : test.path + DIRECTORY_DELIMITER) + test.description
74
78
  console.log(directoryString);
75
79
  passed++;
80
+ clearAllMocks();
76
81
  } catch (error) {
77
82
  const errorDirectory = red(CROSS) + test.path + test.description
78
83
  console.log(errorDirectory);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dannysir/js-te",
3
- "version": "0.0.2",
3
+ "version": "0.1.0",
4
4
  "type": "module",
5
5
  "description": "JavaScript test library",
6
6
  "main": "index.js",
package/src/mock/store.js CHANGED
@@ -1,17 +1,17 @@
1
- export const __mockRegistry__ = new Map();
1
+ export const mockStore = new Map();
2
2
 
3
3
  export function clearAllMocks() {
4
- __mockRegistry__.clear();
4
+ mockStore.clear();
5
5
  }
6
6
 
7
7
  export function mock(modulePath, mockExports) {
8
- __mockRegistry__.set(modulePath, mockExports);
8
+ mockStore.set(modulePath, mockExports);
9
9
  }
10
10
 
11
11
  export function unmock(modulePath) {
12
- __mockRegistry__.delete(modulePath);
12
+ mockStore.delete(modulePath);
13
13
  }
14
14
 
15
15
  export function isMocked(modulePath) {
16
- return __mockRegistry__.has(modulePath);
16
+ return mockStore.has(modulePath);
17
17
  }
package/src/tests.js CHANGED
@@ -3,22 +3,45 @@ import {DIRECTORY_DELIMITER} from "../constants.js";
3
3
  export class Tests {
4
4
  #tests = [];
5
5
  #testDepth = [];
6
+ #beforeEachArr = [];
6
7
 
7
8
  describe(str, fn) {
8
9
  this.#testDepth.push(str);
10
+ const prevLength = this.#beforeEachArr.length;
9
11
  fn();
12
+ this.#beforeEachArr.length = prevLength;
10
13
  this.#testDepth.pop();
11
14
  }
12
15
 
13
16
  test(description, fn) {
17
+ const beforeEachHooks = [...this.#beforeEachArr];
18
+
14
19
  const testObj = {
15
20
  description,
16
- fn,
21
+ fn: async () => {
22
+ for (const hook of beforeEachHooks) {
23
+ await hook();
24
+ }
25
+ await fn();
26
+ },
17
27
  path: this.#testDepth.join(DIRECTORY_DELIMITER),
18
28
  }
19
29
  this.#tests.push(testObj);
20
30
  }
21
31
 
32
+ testEach(cases) {
33
+ return (description, fn) => {
34
+ cases.forEach(testCase => {
35
+ const args = Array.isArray(testCase) ? testCase : [testCase];
36
+ this.test(this.#formatDescription(args, description), () => fn(...args));
37
+ });
38
+ };
39
+ }
40
+
41
+ beforeEach(fn) {
42
+ this.#beforeEachArr.push(fn);
43
+ }
44
+
22
45
  getTests() {
23
46
  return [...this.#tests];
24
47
  }
@@ -26,5 +49,24 @@ export class Tests {
26
49
  clearTests() {
27
50
  this.#tests = [];
28
51
  this.#testDepth = [];
52
+ this.#beforeEachArr = [];
53
+ }
54
+
55
+ #formatDescription(args, description) {
56
+ let argIndex = 0;
57
+ return description.replace(/%([so])/g, (match, type) => {
58
+ if (argIndex >= args.length) return match;
59
+
60
+ const arg = args[argIndex++];
61
+
62
+ switch (type) {
63
+ case 's':
64
+ return arg;
65
+ case 'o':
66
+ return JSON.stringify(arg);
67
+ default:
68
+ return match;
69
+ }
70
+ });
29
71
  }
30
72
  }