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", () => {
// 相当于结束值默认为 0
expect(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 发布了这个工具库,使其可以被其他开发者使用。