These metrics capture the testability dimensions of JavaScript code (controllability, observability, complexity, asynchrony, encapsulation, and side effects) in a way that reflects the dynamic and event-driven nature of the language.
Each metric includes its purpose, definition, formula, and static analysis approach.
| Metric | Purpose | Definition / Formula | Key Static Analysis Features |
|---|---|---|---|
| C: Controllability | Measures how easily inputs can steer execution. | C = wP·#params + wD·#defaults + wE·isExported − wG·#globalRefs − wCl·#closureRefs − wSI·#staticImportsNewables |
Count parameters and defaults; identify global/closure refs; detect exports and hard-coded dependencies. |
| O: Observability | Measures how easily outputs or effects can be observed. | O = wR·hasReturnValue + wL·#logWrites + wIO·#ioWrites − wH·#hiddenStateWrites |
Detect return statements, console/DOM/FS/network writes, and hidden state mutations. |
| BC: Branching Complexity | Captures control-flow complexity that increases test effort. | BC = (#if + #elseIf + #switch + #ternary + #tryCatch + #loops) + nestingDepth |
Count branching and loop constructs; compute nesting depth from AST. |
| AC: Asynchronous Complexity | Quantifies complexity from async constructs. | AC = #await + #thenCatchFinally + #PromiseAllRaceAny + #timers + chainDepth |
Identify async/await, promises, callbacks, and timers. |
| EC: Event-Driven Complexity | Measures reactivity and event-based control. | EC = #domListeners + #nodeEventEmitter + #customEmitter + #addEventListenerCalls + #reactHandlers + #routerHandlers |
Detect addEventListener, .on/.emit, Express routes, React/Vue event handlers. |
| EL: Encapsulation Level | Indicates reliance on hidden or private state. | EL = #closureReads + #closureWrites + #moduleSingletonAccess + #prototypePrivateHeuristics |
Track closure access, singleton mutations, and private/prototype field usage. |
| SEI: Side-Effect Index | Measures side-effect density relative to function size. | SEI = #sideEffectCalls / LOCf |
Identify effectful APIs (fs, fetch, DOM, timers, randomness) and compute per-LOC ratio. |
| ER: Entry-Point Reachability | Marks whether function is reachable from an external entry point. | ER ∈ {0,1} |
Detect exported functions, CLI commands, route handlers, and reachable nodes in call graph. |
| CTS: Composite Testability Score | Aggregates normalized metrics into a single testability measure. | CTS = α·Ĉ + β·Ō − γ·BĈ − δ·ÂĈ − ε·ÊĈ − ζ·ĖĻ − η·ŜĖĨ + κ·ER |
Normalize metrics (0–1), assign weights, and combine controllability, observability, and complexity dimensions. |
Legend:
- Positive metrics (↑ improves testability): C, O, ER
- Negative metrics (↑ decreases testability): BC, AC, EC, EL, SEI
- Composite score (CTS) summarizes overall testability across all dimensions.
Purpose:
How easily can tests steer the execution of the code through inputs?
Definition:
Measures how exposed the function is to external control (parameters, exports) vs. hidden dependencies (globals, closures).
Formula:
C = wP·#params + wD·#defaults + wE·isExported
− wG·#globalRefs − wCl·#closureRefs − wSI·#staticImportsNewables
Static Analysis:
- Count parameters and default values.
- Identify references to undeclared variables (globals/closures).
- Detect exported functions (
export,module.exports,exports.*). - Penalize direct instantiations of imported classes or singletons.
Purpose:
How easily can a test observe the function’s effects or outputs?
Definition:
Measures return values, logging, and visible effects (DOM, FS, network).
Formula:
O = wR·hasReturnValue + wL·#logWrites + wIO·#ioWrites
− wH·#hiddenStateWrites
Static Analysis:
- Check for
returnstatements with values. - Detect
console.*, DOM updates, file or network operations. - Penalize mutations to hidden or closure-scoped state.
Purpose:
Quantifies internal control-flow complexity.
Definition:
Counts conditional and looping structures, plus nesting.
Formula:
BC = (#if + #elseIf + #switch + #ternary + #tryCatch + #loops) + nestingDepth
Static Analysis:
- Count control structures (
IfStatement,SwitchStatement,TryStatement,For*,While*). - Track nesting depth during AST traversal.
Purpose:
Captures how difficult it is to coordinate and observe asynchronous behavior.
Definition:
Based on async/await usage, promise chains, and timers.
Formula:
AC = #await + #thenCatchFinally + #PromiseAllRaceAny + #timers + chainDepth
Static Analysis:
- Count
AwaitExpression,.then/.catch/.finallycalls. - Identify
Promise.all,Promise.race, etc. - Count timers (
setTimeout,setInterval,queueMicrotask).
Purpose:
Reflects difficulty from callbacks, listeners, and reactive flows.
Definition:
Counts event-driven constructs in browser, Node.js, or frameworks.
Formula:
EC = #domListeners + #nodeEventEmitter + #customEmitter
- #addEventListenerCalls + #reactHandlers + #routerHandlers
Static Analysis:
- Detect
addEventListener,on*=handlers. - Identify
.on,.emit,.oncecalls on EventEmitters. - Heuristically detect Express routes or React/Vue handler props.
Purpose:
Estimates how dependent the function is on hidden or private state.
Definition:
Measures closure variable access, private/prototype fields, and module singletons.
Formula:
EL = #closureReads + #closureWrites + #moduleSingletonAccess + #prototypePrivateHeuristics
Static Analysis:
- Identify free variables (not declared locally).
- Detect writes to top-level objects or private class fields (
#name,_name). - Count access to shared state (module scope or singleton patterns).
Purpose:
Quantifies how side-effect-prone a function is relative to its size.
Definition:
Ratio of side-effecting calls to total lines of code (LOC).
Formula:
SEI = #sideEffectCalls / LOCf
Static Analysis:
- Recognize known effectful APIs:
fs.*,fetch,axios,process.*,localStorage, DOM writes, timers,Math.random,Date.now. - Compute LOC per function.
Purpose:
Determines whether a function is reachable from a real application entry point.
Definition:
Boolean reachability flag based on exports, event handlers, or call graph traversal.
Formula:
ER ∈ {0, 1}
Static Analysis:
- Mark exported or top-level invoked functions as roots.
- Use an intra-project call graph to mark reachable functions.
Purpose:
Combines all metrics into a single normalized score for ranking and comparison.
Formula:
CTS = α·Ĉ + β·Ō − γ·BĈ − δ·ÂĈ − ε·ÊĈ − ζ·ĖĻ − η·ŜĖĨ + κ·ER
Where each metric is normalized (m̂ ∈ [0,1]) and weighted.
Interpretation:
Higher CTS → easier to control, observe, and test;
Lower CTS → complex, hidden, and hard to validate.
| Component | Purpose | Library |
|---|---|---|
| Parser | Parse JS files | @babel/parser |
| Traversal | Walk and analyze AST | @babel/traverse |
| Scope tracking | Identify local vs. closure/global | path.scope.hasBinding(name) |
| Config | Store API allowlists, thresholds, weights | JSON/YAML |
| Output | Structured metric results | JSON per function/file/project |
{
"id": "src/utils/format.js:formatDate",
"file": "src/utils/format.js",
"metrics": {
"C": { "params": 2, "globals": 0, "closures": 1, "score": 2.0 },
"O": { "hasReturn": true, "logs": 0, "ioWrites": 0, "score": 1.0 },
"BC": { "branches": 3, "nesting": 1, "score": 4 },
"AC": { "awaits": 0, "thens": 0, "timers": 0, "score": 0 },
"EC": { "listeners": 0, "emitters": 0, "score": 0 },
"EL": { "closureReads": 1, "closureWrites": 0, "score": 1 },
"SEI": { "calls": 0, "loc": 22, "score": 0 },
"ER": 1,
"CTS": 0.83
}
}These metrics collectively capture the multi-dimensional nature of testability in JavaScript, accounting for control, observation, complexity, and hidden behavior.
Each metric contributes to explaining why certain code is easier or harder to test.
| Pattern / Property | Captured By | Example / Explanation |
|---|---|---|
| Heavy global or closure use | C, EL | Reduces controllability; behavior depends on hidden or shared state (window.config, process.env). |
| Lack of explicit return / observable output | O, EL | Tests cannot easily assert outcomes when only side effects occur. |
| Deep branching or nested logic | BC | Increases number of possible execution paths that need testing. |
| Complex async chains | AC | Promises, callbacks, and timers make sequencing and observation difficult. |
| Event-driven behavior | EC | Asynchronous and reactive patterns make triggering conditions non-deterministic. |
| Hidden shared state / module singletons | EL, SEI | Introduces coupling between tests and execution order. |
| High side-effect density | SEI | More external interactions (I/O, network, DOM) make testing expensive or flaky. |
| Unreachable or unused code | ER | Lowers practical test relevance; unreachable functions are not worth testing. |
| High composite score (CTS) | — | Indicates functions that are easy to test in isolation, with clear inputs and outputs. |
| Low composite score (CTS) | — | Signals functions that likely require refactoring or special testing strategies. |
To compare testability metrics across functions, files, and projects, values should be normalized and aggregated systematically.
- Normalize each metric to a 0–1 scale per project using:
- Min–Max normalization:
[ m_{norm} = \frac{m - m_{min}}{m_{max} - m_{min}} ] - Or z-score normalization for large datasets: [ m_{z} = \frac{m - \mu_m}{\sigma_m} ]
- Min–Max normalization:
- Ensure consistent “directionality”:
- For positive metrics (C, O, ER) → higher is better.
- For negative metrics (BC, AC, EC, EL, SEI) → invert before aggregation.
| Level | Description | Aggregation |
|---|---|---|
| Function-level | Base metrics per function. | Raw values. |
| File-level | Summarizes testability per file. | Mean, median, stddev, min, max. |
| Project-level | Summarizes entire project. | Weighted mean across files (by LOC or function count). |
After normalization: [ CTS = α·Ĉ + β·Ō − γ·BĈ − δ·ÂĈ − ε·ÊĈ − ζ·ĖĻ − η·ŜĖĨ + κ·ER ] Weights can be equal (default) or empirically tuned.
Each project’s report should include:
- Statistical summary: mean, median, stddev for each metric.
- Histograms: distribution of BC, AC, EC, CTS.
- Scatter plots:
Cvs.O→ controllability–observability relationship.BCvs.CTS→ effect of complexity on testability.
- Top/bottom N functions: rank by CTS to identify refactoring candidates.
- Cross-project comparison: correlate metrics with project metadata (size, domain, test coverage).
- High variability in C or O → inconsistent coding/testability practices.
- High AC/EC + low O → async/event-heavy code is harder to test.
- High EL + SEI → strong coupling or hidden side effects.
- Stable high CTS across files → project with modular, observable, and controllable design.
Goal:
Normalization and reporting turn raw metric values into actionable insights that explain what factors make JavaScript code testable in practice — enabling both quantitative comparison across projects and qualitative interpretation for developers.
This is a structured, end-to-end plan for collecting projects, computing JS testability metrics, aggregating results, and reporting study findings.
Each step lists inputs → actions → outputs and the best tools to use.
Goal: Make runs reproducible and configurable.
- Inputs:
testability.config.json(paths, excludes, thresholds, weights, side-effect/event API lists) - Actions: Load config, validate schema, set defaults.
- Outputs: In-memory
Configobject used by all steps. - Tools:
zodorjsonschemafor config validation.
Goal: Build a diverse dataset of JS repositories.
Could reuse/start with TestPilot benchmarks.
- Inputs: Candidate repo list (GitHub URLs), inclusion criteria.
- Actions: Clone/shallow-clone or download archives; record metadata (stars, domain, LOC).
- Outputs:
projects/directory;dataset.csvwith project metadata. - Tools:
simple-git,nodegit,cloc(orsloc), GitHub REST API (optional).
Goal: Find analyzable JS files and exclude noise.
- Inputs: Project root, config excludes.
- Actions: Glob files; filter
node_modules,dist,coverage, generated code. - Outputs: List of JS files to parse.
- Tools:
fast-globorglobby.
Goal: Parse JS to ESTree/Babel AST.
- Inputs: JS files, parser options.
- Actions: Parse with tolerant options; support ESM/CJS, JSX if present.
- Outputs: AST per file.
- Tools:
@babel/parser(sourceType: 'unambiguous', plugins:jsx,classProperties,classPrivateProperties,optionalChaining,dynamicImport).
Goal: Walk ASTs, know what’s local vs. closure/global.
- Inputs: ASTs.
- Actions: Traverse; compute lexical scope; resolve identifiers (local/outer/global).
- Outputs: Visitor hooks and scope helpers for metric passes.
- Tools:
@babel/traverse,@babel/types; scope viapath.scope.hasBinding(name); optionaleslint-scopefor explicit scope graphs.
Goal: Enumerate all functions/methods and entry points.
- Inputs: ASTs.
- Actions: Find
FunctionDeclaration, function expressions, arrow functions, class methods; detect exports; tag likely entry points (routes, CLI, handlers). - Outputs:
functions.jsonl(one row/object per function) with stable IDs:file:lineStart:functionName. - Tools: Babel traverse; heuristics for exports (
export,module.exports), Express routes, EventEmitter calls.
Goal: Enable reachability (ER) and slice context.
- Inputs: Function nodes, call expressions, import/export graph.
- Actions: Map callers → callees; resolve same-file symbols; basic inter-file via import names.
- Outputs:
call-graph.json(adjacency list). - Tools: Babel traverse; module graph via
resolve,enhanced-resolve, ormadge(optional).
Compute raw counts first; normalize later. Keep counts and a score per metric if needed.
- Actions: Count params/defaults, exported flag; penalize closure/global refs,
newon imported classes. - Outputs:
{ params, defaults, globals, closures, newables, score } - Tools: Babel traverse; scope checks; import table.
- Actions: Detect returned values;
console.*; DOM writes; Node I/O/network; hidden state writes. - Outputs:
{ hasReturn, logs, ioWrites, hiddenWrites, score } - Tools: API allowlists (config); pattern match known sinks.
- Actions: Count if/switch/ternary/try/loops; track
nestingDepth. - Outputs:
{ branches, nestingDepth, score } - Tools: Traverse; maintain depth counter stack.
- Actions: Count
await;.then/.catch/.finally;Promise.all/race/any; timers; estimatechainDepth. - Outputs:
{ awaits, thens, timers, chainDepth, score } - Tools: Traverse; simple chain analysis.
- Actions: Count DOM listeners; EventEmitter
.on/.emit; Express routes; React/Vue handler heuristics. - Outputs:
{ domListeners, emitters, routerHandlers, reactHandlers, score } - Tools: Allowlist of event APIs; framework signatures.
- Actions: Count closure reads/writes, module singleton access, private/prototype heuristics.
- Outputs:
{ closureReads, closureWrites, moduleSingleton, privateHeuristics, score } - Tools: Scope checks; class/private fields detection.
- Actions: Count calls to effectful APIs; compute LOCf; ratio.
- Outputs:
{ sideEffectCalls, loc, sei } - Tools: Allowlist for side effects; LOC via ranges.
- Actions: Mark reachable from roots (exports, routes, CLI) via call graph traversal.
- Outputs:
0/1 - Tools: BFS/DFS over
call-graph.json.
Goal: Make metrics comparable and combine into CTS.
- Inputs: Raw per-function metrics across a project.
- Actions: Min–max normalize per metric to
[0,1](invert “negative” metrics: BC/AC/EC/EL/SEI); compute CTS with weights. - Outputs: Updated per-function records with normalized fields and
CTS. - Tools: In-repo JS utility; (optional)
ml-matrixor simple math.
Goal: Summaries for analysis and reporting.
- Inputs: Per-function results.
- Actions:
- File level: mean/median/stddev min/max; % low-testability; list worst functions.
- Project level: same stats over all functions; predefined histogram buckets.
- Outputs:
files.json,project-summary.json(with histograms & top/bottom lists). - Tools: In-repo JS utilities; optional
duckdbfor larger datasets.
Goal: Correlate static metrics with runtime testing signals.
- Inputs: Test runs & coverage reports.
- Actions: Run test suite and collect coverage; map covered lines to functions; (optional) mutation testing scores.
- Outputs:
coverage-map.json, augmented summaries. - Tools:
nyc/Istanbul,vitest --coverage,jest --coverage,stryker(optional).
Goal: Produce human-readable project reports and dataset exports.
- Inputs:
project-summary.json,files.json, config. - Actions: Generate Markdown report; export CSVs; (optional) HTML with charts.
- Outputs:
reports/<project>-summary.md,exports/*.csv. - Tools: Markdown template with
mustache/handlebars; (optional)vega-litefor charts.
Goal: Keep the pipeline stable.
- Inputs: Synthetic JS fixtures covering patterns (pure functions, closures, async chains, events, heavy side effects).
- Actions: Unit tests for each metric pass; snapshot tests for file/project aggregation.
- Outputs:
npm testgreen; snapshots under__snapshots__/. - Tools:
vitestorjest.
Goal: One command, full pipeline.
- Inputs: CLI args (
--path,--out,--threshold,--buckets,--exclude). - Actions: Run steps 2→11; write outputs.
- Outputs:
output/+reports/directories per project. - Tools:
commanderoryargs; GitHub Actions workflow for CI runs.
Goal: Turn numbers into findings.
- Inputs: All project summaries + metadata.
- Actions: Compute cross-project statistics; correlations; comparative plots; identify patterns & anti-patterns.
- Outputs: Paper figures/tables; “Implications for practice” and “Threats to validity”.
- Tools:
- JS:
arquero,vega-lite - or Python:
pandas,scipy,seaborn(optional offline analysis)
- JS:
docs/schemas/js-function.md— function record schemadocs/schemas/file-summary.md— file summary schema (freeze)docs/schemas/project-summary.md— project summary schema (freeze)docs/metrics.md— metric definitions & formulasdocs/task-plans/— weekly plans
output/ functions.jsonl # 1 JSON per function (raw + normalized) files.json # file-level aggregates project-summary.json # project-level summary + histograms reports/ -summary.md # human-readable report exports/ functions.csv files.csv