# How to Write Tests for DI/Scope Components Using the InstantiationService Harness

> Learn to write tests for DI/Scope components using the InstantiationService harness. Discover test helpers like stub mock and spy for effective component testing in your projects.

- Repository: [Moonshot AI/kimi-code](https://github.com/MoonshotAI/kimi-code)
- Tags: how-to-guide
- Published: 2026-08-16

---

**TLDR:** Use `TestInstantiationService` from [`packages/agent-core/src/di/testInstantiationService.ts`](https://github.com/MoonshotAI/kimi-code/blob/main/packages/agent-core/src/di/testInstantiationService.ts)—a test-only subclass that exposes protected resolution methods, adds stubbing helpers (`stub`, `mock`, `spy`), and implements `IDisposable` for clean teardown.

The **Kimi Code** codebase includes a lightweight, purpose-built harness for testing dependency injection and scoped service containers. Built directly on top of the production `InstantiationService`, this harness lets you verify DI behavior without spinning up the full application runtime. The following guide walks through the core concepts and practical patterns for writing robust, isolated tests for DI/Scope components.

---

## Understanding the Test Harness Architecture

The test harness centers on three interconnected pieces defined in **[`packages/agent-core/src/di/testInstantiationService.ts`](https://github.com/MoonshotAI/kimi-code/blob/main/packages/agent-core/src/di/testInstantiationService.ts)**:

### TestInstantiationService Class

`TestInstantiationService` extends the production `InstantiationService` with test-specific capabilities:

- **Exposes `_getOrCreateServiceInstance`** via a public `get` method for direct service resolution
- **Adds stubbing utilities:** `mock`, `stub`, `stubPromise`, and `spy` for controlling dependency behavior
- **Implements proper cleanup:** custom `dispose` restores Sinon stubs and optionally calls the parent disposal logic

The class signature includes a `properDispose` flag that determines whether `super.dispose()` runs—useful when you need to test lifecycle behavior without destroying parent containers.

### createServices Factory Helper

The **`createServices`** function (lines 96-135 in [`testInstantiationService.ts`](https://github.com/MoonshotAI/kimi-code/blob/main/testInstantiationService.ts)) provides a one-line setup:

```typescript
function createServices(
  disposables: DisposableStore,
  services: [ServiceIdentifier<T>, T | SyncDescriptor<T>][]
): TestInstantiationService

```

This helper instantiates `TestInstantiationService`, populates a `ServiceCollection` with your service tuples, and registers the container with a disposable store for automatic cleanup.

### ServiceCollection Integration

The test harness uses the same **`ServiceCollection`** class as production code. This mutable map stores either concrete instances or `SyncDescriptor` wrappers that the container instantiates lazily. Using the real collection ensures your tests exercise identical resolution paths.

---

## Basic Resolution Testing

Start with simple service registration and retrieval to verify your component receives correct dependencies.

```typescript
import { createServices } from '#/di/test';
import { ServiceCollection } from '#/di/serviceCollection';
import { IMyService, MyService } from './myService';
import { DisposableStore } from '#/di/lifecycle';

test('my component receives the correct service', () => {
  const disposables = new DisposableStore();

  // Register the concrete implementation for IMyService
  const inst = createServices(disposables, [[IMyService, MyService]]);

  // Resolve the service via the test container
  const service = inst.get(IMyService);

  expect(service).toBeInstanceOf(MyService);
});

```

**Key implementation detail:** The call to `inst.get` delegates to `TestInstantiationService.get`, which exposes the protected `_getOrCreateServiceInstance` method from the base `InstantiationService` class defined in [`packages/agent-core/src/di/instantiationService.ts`](https://github.com/MoonshotAI/kimi-code/blob/main/packages/agent-core/src/di/instantiationService.ts).

---

## Stubbing Dependencies with TestInstantiationService

Replace real implementations with controlled stubs to isolate the unit under test.

```typescript
import { createServices } from '#/di/test';
import { IConfigService } from './config';
import { MyComponent } from './myComponent';

test('component uses stubbed config', () => {
  const disposables = new DisposableStore();

  // Create an empty container, then stub the config service
  const inst = createServices(disposables, []);
  const stub = { getValue: () => 'stubbed' };
  inst.stub(IConfigService, stub);

  const component = new MyComponent(inst.get(IConfigService));
  expect(component.doSomething()).toBe('stubbed');
});

```

**How it works:** `TestInstantiationService.stub` (lines 95-118) inserts the stub directly into the internal `ServiceCollection`, causing all subsequent `get` calls to return your supplied object without invoking the real constructor or factory.

---

## Mocking Class-Based Services

For more sophisticated verification, use `mock` to create Sinon mocks that track method calls and enforce expectations.

```typescript
test('service methods are called with expected arguments', () => {
  const inst = createServices(new DisposableStore(), []);
  const mockService = inst.mock(ILoggerService);
  
  mockService.expects('log').once().withArgs('test-message');

  const consumer = new ServiceConsumer(inst.get(ILoggerService));
  consumer.run();

  mockService.verify();
});

```

The `mock` helper automatically creates a Sinon mock for the service identifier and registers it in the collection. When resolved via `inst.get`, you receive the mock object for setting expectations.

---

## Testing Child Scopes and Disposal

Verify that scoped containers inherit correctly and clean up their resources independently.

```typescript
import { createServices } from '#/di/test';
import { IServiceA, ServiceA } from './serviceA';
import { DisposableStore } from '#/di/lifecycle';

test('child scope disposes its own services', () => {
  const disposables = new DisposableStore();

  // Parent container registers ServiceA as a descriptor
  const parent = createServices(disposables, [[IServiceA, ServiceA]]);

  // Child container overrides IServiceA with a mock
  const childCollection = new ServiceCollection();
  const child = parent.createChild(childCollection) as TestInstantiationService;
  const mock = child.mock(IServiceA);

  // Resolve the mock inside the child
  const resolved = child.get(IServiceA);
  expect(resolved).toBe(mock);

  // Dispose the child – Sinon is restored automatically
  child.dispose();

  // Parent container still works with original implementation
  expect(parent.get(IServiceA)).toBeInstanceOf(ServiceA);
});

```

**Critical behavior:** `parent.createChild` returns a new `TestInstantiationService` with its own `ServiceCollection`. The child's `dispose` method first calls `sinon.restore()` to clean up mocks, then calls `super.dispose()` only when `properDispose` is true (the default). This pattern lets you test scope isolation without affecting sibling or parent containers.

---

## Spying on Service Methods

When you need to observe behavior without replacing the implementation, use `spy`:

```typescript
test('observes service interactions', () => {
  const inst = createServices(disposables, [[IMetricsService, MetricsService]]);
  const spy = inst.spy(IMetricsService, 'record');

  const worker = new BackgroundWorker(inst.get(IMetricsService));
  worker.process();

  expect(spy.calledOnce).toBe(true);
  expect(spy.firstCall.args[0]).toBe('processing-complete');
});

```

The `spy` helper wraps the method on the resolved service instance, allowing verification without modifying the service's return values or side effects.

---

## File Reference: Core DI Implementation

| File | Purpose | Key Exports |
|------|---------|-------------|
| [`packages/agent-core/src/di/instantiationService.ts`](https://github.com/MoonshotAI/kimi-code/blob/main/packages/agent-core/src/di/instantiationService.ts) | Production DI container with cyclic-dependency detection, tracing, and child creation | `InstantiationService`, `IInstantiationService` |
| [`packages/agent-core/src/di/testInstantiationService.ts`](https://github.com/MoonshotAI/kimi-code/blob/main/packages/agent-core/src/di/testInstantiationService.ts) | Test harness extending production container | `TestInstantiationService`, `createServices` |
| [`packages/agent-core/src/di/serviceCollection.ts`](https://github.com/MoonshotAI/kimi-code/blob/main/packages/agent-core/src/di/serviceCollection.ts) | Mutable service registry | `ServiceCollection`, `SyncDescriptor` |
| [`packages/agent-core/src/di/instantiation.ts`](https://github.com/MoonshotAI/kimi-code/blob/main/packages/agent-core/src/di/instantiation.ts) | Service identifier decorators | `createDecorator`, `IInstantiationService` interface |

These files work together to ensure your tests exercise the same resolution logic, lifecycle management, and scope inheritance as production code.

---

## Advanced Pattern: Testing Scoped Disposable Cleanup

When services implement `IDisposable`, verify that child scope disposal triggers proper cleanup:

```typescript
test('child scope triggers dispose on owned services', () => {
  const disposables = new DisposableStore();
  const disposeSpy = sinon.spy();

  class DisposableService {
    dispose = disposeSpy;
  }

  const parent = createServices(disposables, []);
  const childCollection = new ServiceCollection();
  childCollection.set(IDisposableService, new DisposableService());

  const child = parent.createChild(childCollection) as TestInstantiationService;
  child.get(IDisposableService);
  
  child.dispose(true); // properDispose = true forces super.dispose()

  expect(disposeSpy.calledOnce).toBe(true);
});

```

Passing `true` to `dispose` ensures the container iterates through its service graph and invokes `dispose()` on any `IDisposable` instances it owns.

---

## Summary

- **`createServices`** provides one-line container setup with automatic disposable registration
- **`TestInstantiationService.get`** exposes protected resolution for direct service access
- **`stub`, `mock`, `spy`** replace or observe dependencies without constructor invocation
- **`createChild`** spawns isolated scopes that inherit parent services and dispose independently
- **`dispose`** cleans up Sinon state and, with `properDispose`, triggers full container teardown

These primitives align your tests with the production DI behavior defined in [`packages/agent-core/src/di/instantiationService.ts`](https://github.com/MoonshotAI/kimi-code/blob/main/packages/agent-core/src/di/instantiationService.ts), eliminating the gap between test doubles and real container resolution.

---

## Frequently Asked Questions

### What is the difference between `stub` and `mock` in TestInstantiationService?

**`stub`** replaces a service identifier with a plain object or partial implementation—useful for simple value overrides. **`mock`** creates a full Sinon mock with expectation methods like `expects` and `verify`—better for behavioral verification. Both store the replacement in the `ServiceCollection` so `inst.get` returns the controlled object.

### When should I use `properDispose` in TestInstantiationService?

Set `properDispose` to `true` (the default) when you want the container to dispose its actual services and clean up the service graph. Set it to `false` only when testing disposal behavior itself or when the parent container manages lifecycle—you can see this flag checked in the custom `dispose` implementation at [`testInstantiationService.ts`](https://github.com/MoonshotAI/kimi-code/blob/main/testInstantiationService.ts).

### How do I verify that services are singletons within a scope?

Request the same service identifier twice and assert reference equality: `expect(inst.get(IService)).toBe(inst.get(IService))`. The `InstantiationService` caches instances per container, so this validates that your `SyncDescriptor` or registered instance behaves as a singleton within that scope.

### Can I use TestInstantiationService with async service initialization?

Yes. For promise-based initialization, use **`stubPromise`** to register a pre-resolved promise: `inst.stubPromise(IService, Promise.resolve(mockService))`. The container treats this as an already-available instance, avoiding async resolution paths in synchronous tests.