Jest项目实战(2): 项目开发与测试

1. 项目初始化

首先,我们需要为开源库取一个名字,并确保该名字在 npm 上没有被占用。假设我们选择的名字是 jstoolpack,并且已经确认该名字在 npm 上不存在。

bash 复制代码
mkdir jstoolpack
cd jstoolpack
npm init -y

2. 安装依赖

接下来,我们需要安装一些开发和测试依赖。我们将使用 TypeScript 进行开发,并使用 Jest 进行单元测试。

js 复制代码
"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"
}
bash 复制代码
npm i @types/jest jest jest-environment-jsdom ts-jest ts-node typescript -D

3. 项目结构

在项目根目录下创建 src(源码目录)和 tests(测试目录)。项目本身不难,该项目是一个类似于 lodash 的工具库项目,会对常见的 array、function、string、object 等提供一些工具方法。

bash 复制代码
mkdir src tests

4. 配置 TypeScript

在项目根目录下创建 tsconfig.json 文件,配置 TypeScript 编译选项。

json 复制代码
{
  "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 测试框架。

js 复制代码
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 的方法,该方法可以生成指定范围的数组:

js 复制代码
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]

对应的源码如下:

ts 复制代码
// 理论上来讲,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;
}

对应的测试代码如下:

ts 复制代码
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 的方法,有些时候字符串过长,那么我们需要进行一些截取

js 复制代码
truncate("1231323423424", 5) ----> 12...
truncate("12345", 5) ----> 12345
truncate("1231323423424", 5, '-') ----> 1231-

对应的源码如下:

ts 复制代码
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;
}

对应的测试代码如下:

ts 复制代码
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 方法,可以对传入的函数做防抖处理

对应的代码如下:

ts 复制代码
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);
  };
}

对应的测试代码如下:

ts 复制代码
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 中添加一个测试脚本。

json 复制代码
{
  "scripts": {
    "test": "jest"
  }
}

运行测试命令:

bash 复制代码
npm test

8. 构建和发布

package.json 中添加构建脚本。

json 复制代码
{
  "scripts": {
    "build": "tsc",
    "test": "jest"
  }
}

构建项目:

bash 复制代码
npm run build

发布到 npm:

bash 复制代码
npm login
npm publish

总结

通过以上步骤,我们成功地搭建了一个简单的 JavaScript 工具库项目 jstoolpack,并实现了 rangetruncatedebounce 三个常用工具方法。我们使用了 TypeScript 进行类型检查,并使用 Jest 进行单元测试,确保代码的健壮性和可靠性。最后,我们通过 npm 发布了这个工具库,使其可以被其他开发者使用。

相关推荐
红绿鲤鱼几秒前
React-自定义Hook与逻辑共享
前端·react.js·前端框架
Domain-zhuo11 分钟前
什么是JavaScript原型链?
开发语言·前端·javascript·jvm·ecmascript·原型模式
小丁爱养花19 分钟前
前端三剑客(三):JavaScript
开发语言·前端·javascript
ZwaterZ27 分钟前
vue el-table表格点击某行触发事件&&操作栏点击和row-click冲突问题
前端·vue.js·elementui·c#·vue
码农六六27 分钟前
vue3封装Element Plus table表格组件
javascript·vue.js·elementui
西凉河的葛三叔31 分钟前
vue3+elementui-plus el-dialog全局配置点击空白处不关闭弹窗
前端·vue3·elementui-plus
徐同保32 分钟前
el-table 多选改成单选
javascript·vue.js·elementui
快乐小土豆~~32 分钟前
el-input绑定点击回车事件意外触发页面刷新
javascript·vue.js·elementui
周三有雨39 分钟前
【面试题系列Vue07】Vuex是什么?使用Vuex的好处有哪些?
前端·vue.js·面试·typescript
木古古181 小时前
使用chrome 访问虚拟机Apache2 的默认页面,出现了ERR_ADDRESS_UNREACHABLE这个鸟问题
前端·chrome·apache