Skip to content

Jest

题目描述

实现一个简化版的 Jest 测试框架核心功能:

  • expect(value).toBe(expected) - 基本相等断言
  • expect(value).not.toBe(expected) - 取反断言
  • 返回 true(通过)或 false(失败)
js
expect(3).toBe(3) // ✅ true
expect(4).toBe(3) // ❌ false
expect(3).not.toBe(3) // ❌ false
expect(4).not.toBe(3) // ✅ true

扩展思考: 如何避免硬编码,提高代码健壮性?

本题考查 测试框架设计链式调用getter 方法 的使用。

核心知识点

1. 测试框架基础

  • 断言 (Assertion): 验证代码行为是否符合预期
  • 测试用例: 包含输入、预期输出和实际输出的比较
  • 匹配器 (Matcher): 如 toBetoEqualtoContain
  • 修饰符: 如 not 用于取反断言结果

2. Object.is() vs ===

  • Object.is(): 更严格的相等比较
  • 特殊情况: NaN === NaN (false) vs Object.is(NaN, NaN) (true)
  • 0 和 -0: 0 === -0 (true) vs Object.is(0, -0) (false)
  • Jest 选择: Jest 的 toBe 内部使用 Object.is

3. getter 方法的妙用

  • 动态计算: 每次访问时执行逻辑
  • 链式调用: 返回 this 实现方法链
  • 状态切换: 通过 getter 改变内部状态

代码实现

javascript
/**
 * @param {any} input
 * @returns {ExpectObject}
 */
export default function myExpect(input) {
  let isReversed = false // 标记是否使用 not 修饰符

  return {
    toBe(checker) {
      const isEqual = Object.is(input, checker)

      // 根据是否使用 not 决定结果
      // not + 相等 = false, not + 不相等 = true
      // 普通 + 相等 = true, 普通 + 不相等 = false
      if ((isReversed && isEqual) || (!isReversed && !isEqual)) {
        return false
      }
      else {
        return true
      }
    },

    get not() {
      isReversed = !isReversed // 切换取反状态
      return this // 返回自身支持链式调用
    },
  }
}

关键技术点

1. 逻辑判断的简化

javascript
// 原始复杂逻辑
if (isReversed) {
  return !isEqual; // not 修饰符:取反结果
} else {
  return isEqual; // 正常情况:直接返回
}

// 简化后的逻辑
if ((isReversed && isEqual) || (!isReversed && !isEqual)) {
  return false;
} else {
  return true;
}

// 进一步简化
return !((isReversed && isEqual) || (!isReversed && !isEqual));

// 德摩根定律
return (!isReversed || !isEqual) && (isReversed || isEqual);

// 最终简化
return isReversed ? !isEqual : isEqual;

2. getter 的链式调用机制

javascript
get not() {
  console.log('Accessing not getter')
  isReversed = !isReversed
  return this // 关键:返回 this 对象
}

// 使用示例
const expectObj = myExpect(3)
expectObj.not.not.not.toBe(4) // 可以多次链式调用

3. Object.is() 的特殊情况

javascript
// 测试 Object.is 与 === 的差异
console.log(Number.NaN === Number.NaN) // false
console.log(Object.is(Number.NaN, Number.NaN)) // true

console.log(0 === -0) // true
console.log(Object.is(0, -0)) // false

console.log(+0 === -0) // true
console.log(Object.is(+0, -0)) // false

// 为什么 Jest 选择 Object.is?
// 1. 更符合数学直觉(NaN 等于自身)
// 2. 区分 +0 和 -0 的细微差别
// 3. 提供更严格的相等性检查

4. 常见陷阱和坑点

  • 状态污染: not 状态在多次调用间可能被污染
  • 链式调用理解: getter 每次都会执行,修改状态
  • 相等比较选择: 选择合适的比较方法很重要
  • 返回值混淆: 断言结果应该返回布尔值

设计模式

javascript
// 建造者模式 + 链式调用
const result = myExpect(value)
  .not // 修饰符
  .toBe(expected) // 断言方法

// 状态机模式
// State: normal -> not -> normal -> not

扩展思考

1. 避免硬编码的改进版本

javascript
function myExpect(input) {
  const modifiers = { isReversed: false }

  const expectObj = {
    toBe(checker) {
      const isEqual = Object.is(input, checker)
      return modifiers.isReversed ? !isEqual : isEqual
    },
  }

  // 动态添加 not getter
  Object.defineProperty(expectObj, 'not', {
    get() {
      modifiers.isReversed = !modifiers.isReversed
      return this
    },
  })

  return expectObj
}

2. 支持更多匹配器

javascript
function myExpectExtended(input) {
  let isReversed = false

  const matchers = {
    toBe(expected) {
      const result = Object.is(input, expected)
      return isReversed ? !result : result
    },

    toEqual(expected) {
      const result = deepEqual(input, expected)
      return isReversed ? !result : result
    },

    toContain(item) {
      const result = Array.isArray(input) && input.includes(item)
      return isReversed ? !result : result
    },

    toBeGreaterThan(num) {
      const result = typeof input === 'number' && input > num
      return isReversed ? !result : result
    },

    toThrow() {
      let threw = false
      try {
        if (typeof input === 'function')
          input()
      }
      catch (e) {
        threw = true
      }
      return isReversed ? !threw : threw
    },
  }

  Object.defineProperty(matchers, 'not', {
    get() {
      isReversed = !isReversed
      return this
    },
  })

  return matchers
}

// 深度相等比较辅助函数
function deepEqual(a, b) {
  if (a === b)
    return true
  if (a == null || b == null)
    return false
  if (typeof a !== typeof b)
    return false

  if (typeof a === 'object') {
    const keysA = Object.keys(a)
    const keysB = Object.keys(b)
    if (keysA.length !== keysB.length)
      return false

    for (const key of keysA) {
      if (!keysB.includes(key))
        return false
      if (!deepEqual(a[key], b[key]))
        return false
    }
    return true
  }

  return false
}

3. 支持异步断言

javascript
function myExpectAsync(input) {
  let isReversed = false

  return {
    async toResolve() {
      try {
        await input
        return !isReversed
      }
      catch (e) {
        return isReversed
      }
    },

    async toReject() {
      try {
        await input
        return isReversed
      }
      catch (e) {
        return !isReversed
      }
    },

    get not() {
      isReversed = !isReversed
      return this
    },
  }
}

// 使用示例
const promise1 = Promise.resolve(42)
const promise2 = Promise.reject('error')

console.log(await myExpectAsync(promise1).toResolve()) // true
console.log(await myExpectAsync(promise2).not.toResolve()) // true

内容基于 MIT 许可 | 保持节奏 · 持续积累