Allow using `vi.mock` (and related) functions in tests
- 主要語言
- TypeScript
- 星號
- 27k
- 分支
- 11.8k
- 平均合併
- 14 小時 23 分鐘
- 30 天內合併 PR
- 162
描述
### Which @angular/* package(s) are relevant/related to the feature request?
core
### Description
With the new Angular unit-test builder (Vite), test files are bundled into unified chunks.
Because of this full bundling step:
- ESM modules are statically linked at build time
- There is no runtime module graph available
- vi.mock() cannot intercept module loading
- Mocking entire modules (components/services/modules) is not possible
Currently if we use `vi.mock` function for automocking component/directives/services/pipes/etc..., we got an error:
```
Error: The "vi.mock" and related methods are not supported with the Angular unit-test system. Please use Angular TestBed for mocking.
```
TestBed overrides are insufficient because:
- They replace Angular metadata (providers/imports), not module implementation
- They cannot replace pure TS logic or side effects
- They do not intercept ESM import bindings
### Proposed solution
Instead of runtime interception (like Vitest normally does), Angular test builder should:
1. Detect `vi.mock()` calls at compile time (in spec files, and in setupFiles)
2. Hoist them
3. Rewrite import bindings to use a generated mock registry
4. Replace module resolution during bundling
I suggest to create esbuild plugin, that:
1. Parse test file AST
2. Detect:
- `vi.mock()`
- `vi.unmock()`
- `vi.doMock()`
- `vi.doUnmock()`
- `vi.importMock()`
- `vi.importActual()`
- `vi.hoisted()`
3. Hoist mock calls
4. Rewrite imports
#### Example Transform Injectable
**Before**:
```typescript
import { MyService } from './my-service';
import { MyOtherService } from './my-other-service';
vi.mock('./my-service', () => ({
MyService:
@Injectable({providedIn: 'root'})
class {
get() { return 'mock'; }
}
}));
vi.mock('./my-other-service');
test(() => {
const myService = new MyService();
const myOtherService = new MyOtherService();
});
```
**After**:
```typescript
// should initialize once in one environment
const __angularViMocks = (globalThis.__angular_vi_mocks ??= new Map());
__angularViMocks.set(
'./my-service',
(() => ({
MyService: class {
get() { return 'mock'; }
static ɵprov = {
providedIn: 'root',
factory: () => new this();
};
static ɵfac = () => new this();
}
}))()
);
__angularViMocks.set(
'./my-other-service',
(() => ({
MyOtherService: class {
get = vi.fn(); // maybe better to put `vi.fn()` to MyOtherService.prototype.get = vi.fn();
static ɵprov = {
providedIn: 'root',
factory: () => new this();
};
static ɵfac = () => new this();
}
}))()
);
import * as __angularViActualMod1 from './my-service';
import * as __angularViActualMod2 from './my-other-service';
// We should replace that import in every dependent chunk.
const { MyService } = __angularViMocks.get('./my-service') ?? __angularViActualMod1;
const { MyOtherService } = __angularViMocks.get('./my-other-service') ?? __angularViActualMod2;
```
### Requirements:
- Must mock full implementation (not only metadata) for any modules, as `vi.mock()` does
- Must stub components, services, modules, pipes, directives both metadata and implementation
- Must preserve ESM live bindings semantics
- Must preserve sourcemaps and coverage
- Must not require runtime module loader
### Alternatives considered
Currently we have no choice, but stay with slow and inefficient `jest` + `jest-preset-angular`.
I've implemented deep-automocking infrastructure for `jest.mock()` [ng-automocks-jest](https://www.npmjs.com/package/ng-automocks-jest), it can stub any angular entities, both metadata and implementation.
貢獻指南
研究方向
從新的 Angular 單元測試 builder 及其 Vite bundling 路徑開始,接著追蹤 vi.mock 呼叫在哪些地方遭到拒絕,以及 spec 檔案和 setupFiles 如何被轉換。檢視關於編譯期偵測、hoisting、import 重寫和 mock registry 的提議需求。完成的標準是支援列出的 vi 函式,同時保留 ESM live bindings、sourcemaps、coverage,且不使用 runtime module loader。
由索引模型根據 Issue 內容生成。
評估
- 技術堆疊
- angular, typescript, vite
- 領域
- build-system, testing-qa
- Issue 類型
- 功能
- 難度
- 5/5
- 預估耗時
- 一週以上
- 活躍度
- 停滯
- 描述清晰度
- 需要釐清
- 新手友好度
- 20/100