Conversation
- 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
✅ Deploy Preview for rstest-dev ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
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.
| 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); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
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.
| ```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' }); | ||
| ``` |
There was a problem hiding this comment.
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.
| { value }, | ||
| {}, | ||
| ).value as MaybeMockedDeep<T>; |
There was a problem hiding this comment.
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.
| { value }, | |
| {}, | |
| ).value as MaybeMockedDeep<T>; | |
| value, | |
| {}, | |
| ) as MaybeMockedDeep<T>; |
Deploying rstest with
|
| Latest commit: |
b8bee28
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://38ea7115.rstest.pages.dev |
| Branch Preview URL: | https://mock-object.rstest.pages.dev |
Summary
rs.mockObject()for deep mocking objects with optional spy moders.mocked()type helper for TypeScript mock type inferenceRelated Links
Checklist