Jest项目实战(2): 项目开发与测试
1. 项目初始化
首先,我们需要为开源库取一个名字,并确保该名字在 npm 上没有被占用。假设我们选择的名字是 jstoolpack
,并且已经确认该名字在 npm 上不存在。
mkdir jstoolpack
cd jstoolpack
npm init -y
2. 安装依赖
接下来,我们需要安装一些开发和测试依赖。我们将使用 TypeScript 进行开发,并使用 Jest 进行单元测试。
"devDependencies": {"@types/jest": "^29.5.1","jest": "^29.5.0","jest-environment-jsdom": "^29.5.0","ts-jest": "^29.1.0","ts-node": "^10.9.1","typescript": "^5.0.4"
}
npm i @types/jest jest jest-environment-jsdom ts-jest ts-node typescript -D
3. 项目结构
在项目根目录下创建 src
(源码目录)和 tests
(测试目录)。项目本身不难,该项目是一个类似于 lodash 的工具库项目,会对常见的 array、function、string、object 等提供一些工具方法。
mkdir src tests
4. 配置 TypeScript
在项目根目录下创建 tsconfig.json
文件,配置 TypeScript 编译选项。
{"compilerOptions": {"target": "es6","module": "commonjs","strict": true,"esModuleInterop": true,"skipLibCheck": true,"forceConsistentCasingInFileNames": true,"outDir": "./dist","rootDir": "./src","baseUrl": ".","paths": {"*": ["node_modules/*"]}},"include": ["src/**/*.ts"],"exclude": ["node_modules", "dist"]
}
5. 配置 Jest
在项目根目录下创建 jest.config.js
文件,配置 Jest 测试框架。
module.exports = {preset: 'ts-jest',testEnvironment: 'jsdom',roots: ['<rootDir>/tests'],transform: {'^.+\\.tsx?$': 'ts-jest',},moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
};
6. 开发工具方法
6.1 range
方法
这里我们打算扩展一个名为 range 的方法,该方法可以生成指定范围的数组:
range(1, 6) ---> [1, 2, 3, 4, 5] 左闭右开
range(1, 6, 2) ---> [1, 3, 5]
range(1, 6, -2) ---> [1, 3, 5]range(6, 1) ---> [6, 5, 4, 3, 2]
range(6, 1, -2) ---> [6, 4, 2]
range(6, 1, 2) ---> [6, 4, 2]
对应的源码如下:
// 理论上来讲,start,stop,step 都应该是 number 类型
// 但是我们的代码最终是打包为 js 给开发者使用
// 开发者可能会存在各种非常的调用 range() range('a','b','c')
// 因此我们这里打算从方法内部进行参数防御,从而提升我们代码的健壮性
export function range(start?: any, stop?: any, step?: any) {// 参数防御start = start ? (isNaN(+start) ? 0 : +start) : 0;stop = stop ? (isNaN(+stop) ? 0 : +stop) : 0;step = step ? (isNaN(+step) ? 0 : +step) : 1;// 保证 step 的正确if ((start < stop && step < 0) || (start > stop && step > 0)) {step = -step;}const arr: number[] = [];for (let i = start; start > stop ? i > stop : i < stop; i += step) {arr.push(i);}return arr;
}
对应的测试代码如下:
import { range } from "../src/array";test("正常的情况", () => {expect(range(1, 6)).toEqual([1, 2, 3, 4, 5]);expect(range(1, 6, 2)).toEqual([1, 3, 5]);expect(range(6, 1)).toEqual([6, 5, 4, 3, 2]);expect(range(6, 1, -2)).toEqual([6, 4, 2]);
});test("错误的情况", () => {expect(range()).toEqual([]);expect(range("a", "b", "c")).toEqual([]);
});test("测试只传入start", () => {// 相当于结束值默认为 0expect(range(2)).toEqual([2, 1]);expect(range(-2)).toEqual([-2, -1]);
});test("测试step", () => {expect(range(1, 6, -2)).toEqual([1, 3, 5]);expect(range(6, 1, 2)).toEqual([6, 4, 2]);
});
6.2 truncate
方法
这里我们打算提供了一个 truncate 的方法,有些时候字符串过长,那么我们需要进行一些截取
truncate("1231323423424", 5) ----> 12...
truncate("12345", 5) ----> 12345
truncate("1231323423424", 5, '-') ----> 1231-
对应的源码如下:
export function truncate(str?: any, len?: any, omission = "...") {// 内部来做参数防御str = String(str);omission = String(omission);len = len ? Math.round(len) : NaN;if (isNaN(len)) {return "";}if (str.length > len) {// 说明要开始截断str = str.slice(0, len - omission.length) + omission;}return str;
}
对应的测试代码如下:
import { truncate } from "../src/string";test("应该将字符串截取到指定长度", () => {expect(truncate("Hello World", 5)).toBe("He...");expect(truncate("Hello World", 10)).toBe("Hello W...");expect(truncate("Hello World", 11)).toBe("Hello World");expect(truncate("Hello World", 15)).toBe("Hello World");expect(truncate("1231323423424", 5)).toBe("12...");expect(truncate("12345", 5)).toBe("12345");expect(truncate("1231323423424", 5, "-")).toBe("1231-");
});test("如果长度参数不是一个数字,那么返回一个空字符串", () => {expect(truncate("Hello World", NaN)).toBe("");expect(truncate("Hello World", "abc" as any)).toBe("");
});test("应该正确处理空字符串和未定义的输入", () => {expect(truncate("", 5)).toBe("");expect(truncate(undefined, 5)).toBe("un...");
});test("应该正确处理省略号参数", () => {expect(truncate("Hello World", 5, "...")).toBe("He...");expect(truncate("Hello World", 10, "---")).toBe("Hello W---");
});test("始终应该返回一个字符串", () => {expect(typeof truncate("Hello World", 5)).toBe("string");expect(typeof truncate("Hello World", NaN)).toBe("string");expect(typeof truncate(undefined, 5)).toBe("string");
});
6.3 debounce
方法
函数防抖是一个很常见的需求,我们扩展一个 debounce 方法,可以对传入的函数做防抖处理
对应的代码如下:
type FuncType = (...args: any[]) => any;
export function debounce<T extends FuncType>(func: T,wait: number
): (...args: Parameters<T>) => void {let timerId: ReturnType<typeof setTimeout> | null = null;return function (...args: Parameters<T>): void {if (timerId) {clearTimeout(timerId);}timerId = setTimeout(() => {func(...args);}, wait);};
}
对应的测试代码如下:
import { debounce } from "../src/function";beforeEach(() => {jest.useFakeTimers();
});afterEach(() => {jest.clearAllTimers();jest.useRealTimers();
});test("应该在等待时间之后调用函数",()=>{const func = jest.fn();const debouncedFunc = debounce(func, 1000);debouncedFunc();jest.advanceTimersByTime(500);expect(func).toHaveBeenCalledTimes(0);jest.advanceTimersByTime(500);expect(func).toHaveBeenCalledTimes(1);
})test("当防抖函数执行的时候,始终只执行最后一次的调用",()=>{const func = jest.fn();const debouncedFunc = debounce(func, 1000);debouncedFunc('a');debouncedFunc('b');debouncedFunc('c');jest.advanceTimersByTime(1000);expect(func).toHaveBeenCalledWith('c');
})test("在等待时间内又调用了函数,重置计时器",()=>{const func = jest.fn();const debouncedFunc = debounce(func, 1000);debouncedFunc();jest.advanceTimersByTime(500);debouncedFunc();jest.advanceTimersByTime(500);expect(func).toHaveBeenCalledTimes(0);jest.advanceTimersByTime(1000);expect(func).toHaveBeenCalledTimes(1);
})
7. 运行测试
在 package.json
中添加一个测试脚本。
{"scripts": {"test": "jest"}
}
运行测试命令:
npm test
8. 构建和发布
在 package.json
中添加构建脚本。
{"scripts": {"build": "tsc","test": "jest"}
}
构建项目:
npm run build
发布到 npm:
npm login
npm publish
总结
通过以上步骤,我们成功地搭建了一个简单的 JavaScript 工具库项目 jstoolpack
,并实现了 range
、truncate
和 debounce
三个常用工具方法。我们使用了 TypeScript 进行类型检查,并使用 Jest 进行单元测试,确保代码的健壮性和可靠性。最后,我们通过 npm 发布了这个工具库,使其可以被其他开发者使用。