You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JavaScript闭包私有内部函数无法单元测试,是否仍适合采用?

闭包私有函数无法单元测试时,闭包是否仍是合理选择?

我正在练习使用JavaScript闭包,遇到了一个技术疑问:在我的示例实现中,checkEquipment、checkGender、checkCategory、checkPractice、filterExercises这些私有内部函数无法进行单元测试。想请教,在仅能测试最终输出结果、无法覆盖所有私有内部函数的前提下,使用闭包是否仍为合理的技术选择?


JS闭包实现

import exDb from "../../exercises-database/exercises-database.json"
import {
  DatabaseEquipment,
  DatabaseExercise,
  DatabaseExerciseCategories,
  Genders,
  Practice,
  WKGArgs,
  WKGExercises,
} from "../../types"

export function getUserExercises(wkgArgs: WKGArgs): WKGExercises {
  const { practice, gender, nonAvailableEquipment } = wkgArgs

  const exercisesDatabase = exDb as unknown as DatabaseExercise[]

  function checkEquipment(exEquipments: DatabaseEquipment[]): boolean {
    return (
      exEquipments?.filter(
        (exEquipment) => !nonAvailableEquipment.includes(exEquipment)
      ).length > 0
    )
  }

  function checkGender(exGender: Genders): boolean {
    return exGender === gender || exGender === "all"
  }

  function checkCategory(
    exCategories: string[],
    category: DatabaseExerciseCategories
  ): boolean {
    return exCategories.includes(category)
  }

  function checkPractice(exPractice: Practice): boolean {
    return exPractice === practice || exPractice === "all"
  }

  function filterExercises(
    category: DatabaseExerciseCategories
  ): DatabaseExercise[] {
    return exercisesDatabase.filter(
      (exercise) =>
        checkEquipment(exercise.exercise_equipments) &&
        checkGender(exercise.gender) &&
        checkPractice(exercise.practice) &&
        checkCategory(exercise.exercise_categories, category)
    )
  }

  return {
    stretching: filterExercises("étirement"),
    jointUnlocking: filterExercises("déverrouillage articulaire"),
    warmup: filterExercises("échauffement"),
    musculation: filterExercises("musculation"),
    sheathing: filterExercises("gainage"),
    cardio: filterExercises("cardio"),
    competitionPreparation: filterExercises("préparation au concours"),
    abs: filterExercises("abdominaux"),
    cardioWarmUp: filterExercises("cardio échauffement"),
  }
}

单元测试代码

function logCategoryLength(userExercises: WKGExercises): void {
  Object.values(userExercises).forEach((ex, index) => {
    console.log(Object.keys(userExercises)[index], ex.length)
  })
}

const allSimpleUserCases = generateSimpleAllUserCase()

describe("getUserExercises", () => {
  test("BEGINNER INDOOR User get exercises in each categories", () => {
    allSimpleUserCases.forEach((userArgs) => {
      const userExercises = getUserExercises(userArgs)

      logCategoryLength(userExercises)

      const isAllCategoryHaveLength = Object.values(userExercises).every(
        (exercises) => exercises.length > 0
      )

      expect(isAllCategoryHaveLength).toBeTruthy()
    })
  })
})

解答

闭包仍然是合理的技术选择,核心原因和优化方向如下:

1. 闭包的封装价值依然成立

你当前用闭包的核心目的是封装内部校验逻辑,避免全局作用域污染,同时让内部函数能直接访问外部的wkgArgs和exercisesDatabase,这种设计本身是合理的。虽然无法直接测试内部函数,但通过黑盒测试覆盖足够多的输入场景,完全可以验证内部逻辑的正确性——毕竟所有内部函数的逻辑最终都会体现在getUserExercises的输出结果里。

比如针对你的测试代码,可以补充更多场景:

  • 测试某类设备不可用时,对应依赖该设备的练习是否被过滤
  • 测试指定性别为男性时,仅允许男性或通用的练习被保留
  • 测试不同练习等级(比如BEGINNER/ADVANCED)下的输出差异

这些场景能间接覆盖所有内部校验函数的分支逻辑。

2. 若需单独测试内部函数,无需放弃闭包

如果内部逻辑复杂度上升,单独测试能降低调试成本,可以通过以下方式调整,平衡封装性与可测试性:

(1)抽离纯函数

把不依赖闭包环境的通用校验逻辑抽成独立纯函数,导出后单独测试。比如checkGender、checkCategory这类逻辑,本身不依赖外部变量,只是接收输入返回结果:

// 单独导出纯函数,方便测试
export function checkGender(exGender: Genders, targetGender: Genders): boolean {
  return exGender === targetGender || exGender === "all"
}

export function checkCategory(
  exCategories: string[],
  targetCategory: DatabaseExerciseCategories
): boolean {
  return exCategories.includes(targetCategory)
}

// 在闭包内调用
export function getUserExercises(wkgArgs: WKGArgs): WKGExercises {
  const { gender } = wkgArgs
  // ...
  // 用闭包变量作为参数传给纯函数
  function checkGenderWrapper(exGender: Genders): boolean {
    return checkGender(exGender, gender)
  }
  // ...
}

(2)让依赖闭包的函数显式接收参数

对于依赖闭包环境的函数(比如checkPractice依赖外部的practice),可以改成接收参数的形式,或者在测试时模拟闭包环境:

// 调整为纯函数,接收目标practice作为参数
function checkPractice(exPractice: Practice, targetPractice: Practice): boolean {
  return exPractice === targetPractice || exPractice === "all"
}

// 闭包内调用时传入外部变量
function getUserExercises(wkgArgs: WKGArgs): WKGExercises {
  const { practice } = wkgArgs
  // ...
  function checkPracticeWrapper(exPractice: Practice): boolean {
    return checkPractice(exPractice, practice)
  }
  // ...
}

3. 权衡:不要为测试过度破坏封装

如果内部逻辑简单,像你当前的实现一样只是基础校验,没必要为了单独测试拆分函数。黑盒测试覆盖足够场景后,代码的可靠性已经有保障,强行拆分反而会增加模块复杂度。


内容的提问来源于stack exchange,提问作者Jean Baptiste Thery

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 05:31:18