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

