---
title: In a Heft/SPFx project jest.mock() doesn't hoist — and sp-http drags in a missing MS-internal module
short-title: "`jest.mock()` doesn't hoist under Heft"
summary: Tests run over pre-compiled `lib-commonjs` without Babel, so a mock written below the imports lands after `require`; write it above the imports or use `moduleNameMapper` (and why only *some* sp-http suites die on `@msinternal/ecs-flight`)
tags: [spfx, jest, testing, heft, sp-http, mocking]
applies-to: SPFx projects built with Heft (@rushstack/heft-jest-plugin), SharePoint Framework 1.1x+
last-reviewed: 2026-09-24
---

# In a Heft/SPFx project `jest.mock()` doesn't hoist — and `sp-http` drags in a missing MS-internal module

> **Bottom line.** A test suite for a service that touches `SPHttpClient` dies with `Cannot find module '@msinternal/ecs-flight'` before the first test runs. Adding `jest.mock('@microsoft/sp-http', …)` below the imports — where Babel would normally lift it from — doesn't fix it: Heft runs Jest over pre-compiled `lib-commonjs` **without Babel**, so nothing hoists the call above `require()`. Either write `jest.mock()` physically above the imports (TypeScript keeps statement order), or map the module with `moduleNameMapper` in the project's `config/jest.config.json`.
>
> **Ve zkratce.** Testovací sada služby, která sahá na `SPHttpClient`, spadne na `Cannot find module '@msinternal/ecs-flight'` ještě před prvním testem. `jest.mock('@microsoft/sp-http', …)` POD importy – odkud ho jinde vytáhne Babel – to NEspraví: Heft pouští Jest nad předkompilovaným `lib-commonjs` **bez Babelu**, takže volání nad `require()` nikdo nevytáhne. Buď `jest.mock()` napiš fyzicky NAD importy (TypeScript pořadí příkazů zachová), nebo modul namapuj přes `moduleNameMapper` v projektovém `config/jest.config.json`.

## Symptom

One suite fails to run at all, while the others in the same folder pass:

```
● Test suite failed to run
  Cannot find module '@msinternal/ecs-flight' from 'node_modules/@microsoft/sp-core-library/lib-commonjs/SPFlight.js'
  Require stack:
    node_modules/@microsoft/sp-core-library/lib-commonjs/SPFlight.js
    …
    lib-commonjs/…/MyService.js
    lib-commonjs/…/MyService.test.js
```

The test doesn't make any HTTP calls — it only exercises a pure exported function from the same module. The obvious fix (`jest.mock('@microsoft/sp-http', () => ({ SPHttpClient: { configurations: { v1: {} } } }))` added right after the imports, where it usually lives) changes nothing.

## Cause

Two independent layers, and you need both to make sense of it:

**1. Value import vs type-only import.** `@msinternal/ecs-flight` is a Microsoft-internal package that isn't published to your `node_modules`. You only reach it when a real `require('@microsoft/sp-http')` survives compilation — which happens when the module uses something from `sp-http` **as a value**:

```ts
import { SPHttpClient } from '@microsoft/sp-http';
await this._sp.get(url, SPHttpClient.configurations.v1);   // ← value use → require() stays
```

A module that only uses `sp-http` for **types** (`MSGraphClientFactory`, `SPHttpClientResponse`, `ISPHttpClientOptions`) compiles down to *nothing* — TypeScript elides type-only imports, no `require` is emitted, and that suite passes happily. This is why the failure looks arbitrary across sibling test files. Check which case you're in by grepping the **compiled** output, not the source:

```bash
grep -c 'require("@microsoft/sp-http")' lib-commonjs/path/to/MyService.js   # 0 = fine, 1+ = needs a stub
```

**2. No Babel means no hoisting.** A `jest.mock()` written below the imports normally still runs first, because **babel-plugin-jest-hoist** rewrites the file to lift the call above the `require`s. The Heft rig deliberately avoids Babel — it runs Jest over already-compiled JS and skips transforming it:

```jsonc
"roots": ["<rootDir>/lib-commonjs"],
"transformIgnorePatterns": ["[\\/]lib-commonjs[\\/](.*)\\.js$"],
"coverageProvider": "v8",   // "Use v8 coverage provider to avoid Babel"
```

So your compiled test looks like this, and the mock is registered *after* the module graph has already been loaded and thrown:

```js
"use strict";
var MyService_1 = require("./MyService");   // ← blows up here
jest.mock('@microsoft/sp-http', …);          // ← too late, never reached
```

## Fix

### For one test file: put `jest.mock()` above the imports

TypeScript does not reorder statements, so a call you write above the imports stays above the `require`s in `lib-commonjs`:

```ts
jest.mock('@microsoft/sp-http', () => ({ SPHttpClient: { configurations: { v1: {} } } }));   // first
import { buildQuery } from './MyService';   // compiled to require("./MyService") below the mock
```

The mock has to precede **every** import that loads the module transitively, including the import of the module under test. Check the compiled file: in `lib-commonjs/…/MyService.test.js`, `jest.mock(` must come before the first `require(`. (Measured with TypeScript 5.8, and in a real SPFx suite whose compiled test keeps `jest.mock` on the line above the first `require`.)

### For the whole suite: `moduleNameMapper`

This does not depend on anyone remembering the order. Map the module before anything can `require` it, via `moduleNameMapper` in the **project's** `config/jest.config.json` (Heft's Jest plugin looks for exactly that path; `extends` keeps the rig's settings):

```jsonc
{
  "extends": "@microsoft/spfx-web-build-rig/profiles/default/config/jest.config.json",

  "moduleNameMapper": {
    "^@microsoft/sp-http$": "<rootDir>/config/jest/sp-http.stub.js"
  },

  // the stub lives outside lib-commonjs; skip transforming it so it doesn't need a Babel config
  "transformIgnorePatterns": ["[\\/]lib-commonjs[\\/](.*)\\.js$", "[\\/]config[\\/]jest[\\/](.*)\\.js$"]
}
```

The stub only needs to hold the *shape* of the API so module loading succeeds — tests deliberately don't do network I/O:

```js
// config/jest/sp-http.stub.js
const noop = () => Promise.reject(new Error('sp-http stub: no network in tests'));

class SPHttpClient { get() { return noop(); } post() { return noop(); } fetch() { return noop(); } }
SPHttpClient.configurations = { v1: { name: 'sp-http-stub-v1' } };

module.exports = { SPHttpClient, SPHttpClientResponse: class {}, MSGraphClientFactory: class {} };
```

Keep the stub **outside** `lib-commonjs` — that folder is build output and gets wiped by `--clean`.

If a test ever needs to assert on HTTP behaviour, inject a `jest.fn()` double through the service's constructor (these services take an `SPHttpClient` argument anyway) rather than growing this stub.

## Why it's easy to miss

- The error names a package you've never heard of and points at `node_modules`, so it reads like a broken install — `npm ci` doesn't help.
- Sibling suites importing the *same* `@microsoft/sp-http` pass, because theirs are type-only imports that vanish at compile time.
- `jest.mock()` is the reflex answer and works in most React/Node projects; the Heft "no Babel" choice silently removes the hoisting it depends on. It still works when you place it above the imports yourself — which is why "jest.mock doesn't work under Heft" is too strong a conclusion.

## See also

- `gotchas/spfx/es2015-lib-forbidden-apis.md` — other places the SPFx toolchain's compile settings surprise you.
