Skip to content

feat(core): add mockObject and mocked utilities#881

Merged
9aoy merged 5 commits intomainfrom
mock-object
Jan 23, 2026
Merged

feat(core): add mockObject and mocked utilities#881
9aoy merged 5 commits intomainfrom
mock-object

Conversation

@9aoy
Copy link
Copy Markdown
Collaborator

@9aoy 9aoy commented Jan 19, 2026

Summary

  • Add rs.mockObject() for deep mocking objects with optional spy mode
  • Add rs.mocked() type helper for TypeScript mock type inference
class UserService {
  getUser() {
    return { id: 1, name: 'Alice' };
  }
}

const MockedService = rstest.mockObject(UserService);
const instance = new MockedService();

rs.mocked(instance.getUser).mockImplementation(() => ({ id: 2, name: 'Bob' }));
expect(instance.getUser()).toEqual({ id: 2, name: 'Bob' });

Related Links

Checklist

  • Tests updated (or not required).
  • Documentation updated (or not required).

- Add rs.mockObject() for deep mocking objects with optional spy mode
- Add rs.mocked() type helper for TypeScript mock type inference
- Support mocking class constructors with proper ES6 class handling
- Add MockedClass, MaybeMockedDeep, and related types
- Add documentation for both English and Chinese
- Add e2e tests for mockObject functionality
Copilot AI review requested due to automatic review settings January 19, 2026 12:43
@netlify
Copy link
Copy Markdown

netlify bot commented Jan 19, 2026

Deploy Preview for rstest-dev ready!

Name Link
🔨 Latest commit 5096357
🔍 Latest deploy log https://app.netlify.com/projects/rstest-dev/deploys/696ef677cc67aa00085326b6
😎 Deploy Preview https://deploy-preview-881--rstest-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds two new utilities to the rstest testing framework: mockObject for deep mocking objects with optional spy mode, and mocked as a TypeScript type helper for mock type inference.

Changes:

  • Adds mockObject() function that deeply mocks objects, replacing methods with mock functions while preserving primitive values
  • Adds mocked() type helper that provides proper TypeScript types for mocked objects without runtime behavior changes
  • Includes comprehensive type definitions for mocking classes, functions, and nested objects

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
packages/core/src/types/mock.ts Defines type interfaces for MockOptions, MockedClass, MockedFunction, MockedObject, and helper types for deep mocking
packages/core/src/runtime/api/mockObject.ts Implements the core mockObject algorithm with circular reference handling, getter/setter support, and deep property traversal
packages/core/src/runtime/api/spy.ts Adds createMockInstance function for handling class constructors and prototype methods
packages/core/src/runtime/api/utilities.ts Integrates mockObject and mocked utilities into the rstest API
e2e/mock/tests/mockObject.test.ts Provides test coverage for basic mocking, spy mode, arrays, primitives, objects, classes, and the mocked helper
website/docs/en/api/runtime-api/rstest/mock-functions.mdx Documents mockObject and mocked with usage examples in English
website/docs/zh/api/runtime-api/rstest/mock-functions.mdx Documents mockObject and mocked with usage examples in Chinese

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1 to +147
import { describe, expect, rs, test } from '@rstest/core';

describe('rs.mockObject', () => {
test('mocks methods to return undefined by default', () => {
const mocked = rs.mockObject({
method() {
return 42;
},
});

expect(mocked.method()).toBe(undefined);
expect(rs.isMockFunction(mocked.method)).toBe(true);
});

test('keeps method name', () => {
const mocked = rs.mockObject({
myMethod() {
return 42;
},
});

expect(mocked.myMethod.getMockName()).toBe('myMethod');
});

test('mocks nested objects deeply', () => {
const original = {
simple: () => 'value',
nested: {
method: () => 'real',
},
prop: 'foo',
};

const mocked = rs.mockObject(original);
expect(mocked.simple()).toBe(undefined);
expect(mocked.nested.method()).toBe(undefined);
expect(mocked.prop).toBe('foo');
});

test('can mock return values', () => {
const mocked = rs.mockObject({
simple: (): string => 'value',
nested: {
method: (): string => 'real',
},
});

mocked.simple.mockReturnValue('mocked');
mocked.nested.method.mockReturnValue('mocked nested');

expect(mocked.simple()).toBe('mocked');
expect(mocked.nested.method()).toBe('mocked nested');
});

test('with spy option keeps original implementations', () => {
const original = {
simple: () => 'value',
nested: {
method: () => 'real',
},
};

const spied = rs.mockObject(original, { spy: true });
expect(spied.simple()).toBe('value');
expect(spied.simple).toHaveBeenCalled();
expect(spied.simple.mock.results[0]).toEqual({
type: 'return',
value: 'value',
});
});

test('arrays are empty by default', () => {
const { array } = rs.mockObject({
array: [1, 2, 3],
});
expect(array).toEqual([]);
});

test('arrays keep values when spy is true', () => {
const { array } = rs.mockObject(
{
array: [1, 2, 3],
},
{ spy: true },
);
expect(array).toHaveLength(3);
expect(array[0]).toBe(1);
expect(array[1]).toBe(2);
expect(array[2]).toBe(3);
});

test('keeps primitive values', () => {
const mocked = rs.mockObject({
number: 123,
string: 'hello',
boolean: true,
nullValue: null,
});

expect(mocked.number).toBe(123);
expect(mocked.string).toBe('hello');
expect(mocked.boolean).toBe(true);
expect(mocked.nullValue).toBe(null);
});

test('deeply clones objects', () => {
const mocked = rs.mockObject({
obj: { a: 1, b: { c: 2 } },
});

expect(mocked.obj).toEqual({ a: 1, b: { c: 2 } });
});

test('mocks class constructors', () => {
class OriginalClass {
value = 42;
getValue() {
return this.value;
}
}
const MockedClass = rs.mockObject(OriginalClass, { spy: true });
const instance = new MockedClass();
expect(instance.getValue()).toBe(42);
rs.mocked(instance.getValue).mockImplementation(() => 100);
expect(instance.getValue()).toBe(100);
});
});

describe('rs.mocked', () => {
test('returns the same object', () => {
const mock = rs.fn();
const mocked = rs.mocked(mock);
expect(mocked).toBe(mock);
});

test('works with deep option', () => {
const mock = rs.fn();
const mocked = rs.mocked(mock, true);
expect(mocked).toBe(mock);
});

test('works with options object', () => {
const mock = rs.fn();
const mocked = rs.mocked(mock, { partial: true, deep: true });
expect(mocked).toBe(mock);
});
});
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mockObject implementation handles special object types like Date, RegExp, and Map by returning them as-is (line 249 in mockObject.ts), but there are no tests verifying this behavior. Consider adding tests for these special object types to ensure they are handled correctly in both automock and autospy modes.

Copilot uses AI. Check for mistakes.
Comment on lines +160 to +172
```ts
class UserService {
getUser() {
return { id: 1, name: 'Alice' };
}
}

const MockedService = rstest.mockObject(UserService);
const instance = new MockedService();

rs.mocked(instance.getUser).mockImplementation(() => ({ id: 2, name: 'Bob' }));
expect(instance.getUser()).toEqual({ id: 2, name: 'Bob' });
```
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation example for mocking classes doesn't specify the spy option, but the corresponding test (line 121) shows it requires { spy: true } to work correctly. Without the spy option, the class methods would return undefined and wouldn't preserve instance state. Consider either updating the documentation to include the spy option or clarifying that the example assumes spy mode.

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +57
{ value },
{},
).value as MaybeMockedDeep<T>;
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mockObject function is called with the value wrapped in an object { value } and then extracts the value property from the result. This adds unnecessary complexity and could be simplified by passing the value directly to mockObject and having it return the mocked value directly, rather than wrapping and unwrapping.

Suggested change
{ value },
{},
).value as MaybeMockedDeep<T>;
value,
{},
) as MaybeMockedDeep<T>;

Copilot uses AI. Check for mistakes.
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Jan 20, 2026

Deploying rstest with  Cloudflare Pages  Cloudflare Pages

Latest commit: b8bee28
Status: ✅  Deploy successful!
Preview URL: https://38ea7115.rstest.pages.dev
Branch Preview URL: https://mock-object.rstest.pages.dev

View logs

@9aoy 9aoy merged commit 6e563ff into main Jan 23, 2026
10 checks passed
@9aoy 9aoy deleted the mock-object branch January 23, 2026 04:49
@9aoy 9aoy mentioned this pull request Jan 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants