vitest-mock-extended

repository·main·Indexed 42 days ago

https://github.com/eratio08/vitest-mock-extended

Type-safe mocking extensions for Vitest, forked from jest-mock-extended. Version 5.1.1 provides a Jest-like API with enhanced TypeScript support for interfaces, arguments, and return types. Features include the mock() and mockDeep() functions, MockProxy types, the calledWith() extension for argument matching, and a suite of asymmetric matchers such as any(), isA(), and captor() for flexible mock assertions.

Tokens
4K
Snippets
12
Records
16
Agent score
52%

What's inside vitest-mock-extended

  1. Install vitest-mock-extended

    main

    Install the package as a development dependency using npm or yarn.

    Note: If you encounter a ReferenceError: vi is not defined, ensure you have set globals: true in your Vitest configuration.

    npm install vitest-mock-extended --save-dev
    
    # or
    
    yarn add vitest-mock-extended --dev
  2. Use mocked type helpers

    main

    When working with modules mocked via vi.mock, you may not be able to refer to the original object directly due to hoisting. Use these helpers to get type-safe access to mocked values:

    • mocked(obj): Returns the mocked version of an object.
    • mocked(obj, true): Returns a deep mocked version of an object.
    • mockedFn(fn): Returns the mocked version of a function.
    • isMockObject(val): A type guard to check if a value was created by mock() or mockDeep(). It narrows the type to MockProxy<T>.
    import { mocked, mockedFn, isMockObject, type MockProxy } from "vitest-mock-extended";
    import { originalObj, originalFn } from "somewhere";
    
    const mockedObj = mocked(originalObj);
    const deepMockedObj = mocked(originalObj, true);
    const mockedFunction = mockedFn(originalFn);
    
    // Type guard usage
    const service: Service | MockProxy<Service> = getService();
    if (isMockObject(service)) {
        service.request.mockReturnValue("mocked response");
    }
  3. Write a custom matcher

    main

    You can create custom matchers using the MatcherCreator and Matcher classes. By default, the expectedValue and actualValue share the same type. If you need them to differ, provide two generic parameters to MatcherCreator<ActualType, ExpectedType>.

    import { MatcherCreator, Matcher } from 'vitest-mock-extended';
    
    // Simple custom matcher
    export const myMatcher: MatcherCreator<MyType> = (expectedValue) =>
        new Matcher((actualValue) => {
            return expectedValue === actualValue && actualValue.isSpecial;
        });
    
    // Custom matcher with different expected type
    export const myMatcherWithDiffType: MatcherCreator<string[], string> = (expectedValue) =>
        new Matcher((actualValue) => {
            return actualValue.includes(expectedValue);
        });
  4. Create deep mocks with mockDeep()

    main

    If you need to mock an object where nested properties or returned objects also need to be mocks, use mockDeep<T>().

    Configuration Options

    • funcPropSupport: true: Enables support for properties that are themselves functions on the mocked object.
    • fallbackMockImplementation: A function that is called if a return value hasn't been explicitly defined via calledWith.
    import { mockDeep } from 'vitest-mock-extended';
    
    // Basic deep mock
    const mockObj: DeepMockProxy<Test1> = mockDeep<Test1>();
    mockObj.deepProp.getNumber.calledWith(1).mockReturnValue(4);
    
    // Deep mock with function property support
    const mockObjWithFunc = mockDeep<Test1>({ funcPropSupport: true });
    mockObjWithFunc.deepProp.calledWith(1).mockReturnValue(3);
    
    // Deep mock with fallback implementation
    const mockObjWithFallback = mockDeep<Test1>({
        fallbackMockImplementation: () => {
            throw new Error('please add expected return value using calledWith');
        },
    });
  5. Mock an interface with mock()

    main

    Use the mock<T>() function to create a type-safe mock of an interface or object. This allows you to define return values for specific methods and verify calls using Vitest's expect API.

    import { mock } from 'vitest-mock-extended';
    
    interface PartyProvider {
        getPartyType: () => string;
        getSongs: (type: string) => string[];
        start: (type: string) => void;
    }
    
    describe('Party Tests', () => {
        test('Mock out an interface', () => {
            const mock = mock<PartyProvider>();
            mock.start('disco party');
    
            expect(mock.start).toHaveBeenCalledWith('disco party');
        });
    
        test('mock out a return type', () => {
            const mock = mock<PartyProvider>();
            mock.getPartyType.mockReturnValue('west coast party');
    
            expect(mock.getPartyType()).toBe('west coast party');
        });
    });
  6. Assign mocks to typed variables using MockProxy

    main

    When you need to declare a mock variable with a specific type (e.g., in a let declaration for beforeEach), use the MockProxy<T> type. This ensures the variable has access to all mock-specific APIs like calledWith().

    import { MockProxy, mock } from 'vitest-mock-extended';
    
    describe('test', () => {
        let myMock: MockProxy<MyInterface>;
    
        beforeEach(() => {
            myMock = mock<MyInterface>();
        })
    
        test(() => {
             myMock.calledWith(1).mockReturnValue(2);
             ...
        })
    });
  7. Use the calledWith() extension for argument matching

    main

    The calledWith() extension allows you to define different behaviors or expectations based on the arguments passed to a mock function. It supports both exact values and matchers, and is fully type-checked.

    Decorating existing Vitest mocks

    To add calledWith to a function already mocked by Vitest's vi.mock, wrap it with mockFn():

    import { vi } from 'vitest';
    import { mockFn } from 'vitest-mock-extended';
    import { aFunction } from './someThingIWantToMock';
    
    vi.mock('./someThingIWantToMock');
    
    const mockedAFunction = mockFn(aFunction);
    // Now you can use calledWith
    mockedAFunction.calledWith('foobar').mockReturnValue(true);
    const provider = mock<PartyProvider>();
    provider.getSongs.calledWith('disco party').mockReturnValue(['Dance the night away', 'Stayin Alive']);
    expect(provider.getSongs('disco party')).toEqual(['Dance the night away', 'Stayin Alive']);
    
    // Using Matchers
    provider.getSongs.calledWith(any()).mockReturnValue(['Saw her standing there']);
    provider.getSongs.calledWith(anyString()).mockReturnValue(['Saw her standing there']);
    
    // Using mockFn for standalone functions
    type MyFn = (x: number, y: number) => Promise<string>;
    const fn = mockFn<MyFn>();
    fn.calledWith(1, 2).mockReturnValue('str');
  8. Clear or Reset mocks

    main

    Use mockClear or mockReset to reset the state of your mocks, providing the same functionality as Vitest's vi.fn() utilities.

    import { mock, mockClear, mockReset } from 'vitest-mock-extended';
    
    describe('test', () => {
       const mock: UserService = mock<UserService>();
    
       beforeEach(() => {
          mockReset(mock); // or mockClear(mock);
       });
    });
  9. Reference: Available Matchers

    main

    The following matchers are available for use within calledWith() to provide flexible argument expectations.

    | Matcher | Description |
    | ---------------------- | ---------------------------------------------------------- |
    | any() | Matches any arg of any type. |
    | anyBoolean() | Matches any boolean (true or false) |
    | anyString() | Matches any string including empty string |
    | anyNumber() | Matches any number that is not NaN |
    | anyFunction() | Matches any function |
    | anyObject() | Matches any object (typeof m === 'object') and is not null |
    | anyArray() | Matches any array |
    | anyMap() | Matches any Map |
    | anySet() | Matches any Set |
    | isA(class) | e.g isA(DiscoPartyProvider) |
    | includes('value') | Checks if value is in the argument array |
    | containsKey('key') | Checks if the key exists in the object |
    | containsValue('value') | Checks if the value exists in an object |
    | has('value') | checks if the value exists in a Set |
    | notNull() | value !== null |
    | notUndefined() | value !== undefined |
    | notEmpty() | value !== undefined && value !== null && value !== '' |
    | captor() | Used to capture an arg - alternative to mock.calls[0][0] |
  10. Use asymmetric matchers for Vitest expectations

    main

    The vitest-mock-extended library provides a set of asymmetric matchers that can be used within Vitest's expect() calls (e.g., in toHaveBeenCalledWith). These matchers allow you to verify that a mock was called with specific types or properties rather than exact values.

    Commonly used matchers include:

    • any(): Matches anything.
    • anyString(), anyNumber(), anyBoolean(), anyFunction(), anySymbol(), anyObject(): Matches values of the specified type.
    • anyArray(), anyMap(), anySet(): Matches specific collection types.
    • isA(Class): Matches if the value is an instance of the provided class.
    • notNull(), notUndefined(), notEmpty(): Matches values that are not null, not undefined, or not an empty string.
    • arrayIncludes(value), setHas(value), mapHas(value): Matches collections that contain a specific value.
    • objectContainsKey(key), objectContainsValue(value): Matches objects that contain a specific key or value.
    import { anyString, isA, anyArray } from 'vitest-mock-extended';
    import { expect, vi }
    
    const myMock = vi.fn();
    
    // Example usage in expectations
    myMock('hello');
    expect(myMock).toHaveBeenCalledWith(anyString());
    expect(myMock).toHaveBeenCalledWith(isA(MyClass));
    expect(myMock).toHaveBeenCalledWith(anyArray());
  11. Create custom matchers with matches()

    main

    You can create a custom asymmetric matcher by using the matches() function. This function takes a MatcherFn<T>, which is a function that receives the actual value and returns a boolean.

    This is useful for complex validation logic that isn't covered by the built-in matchers.

    import { matches } from 'vitest-mock-extended';
    import { expect, vi }
    
    const myMock = vi.fn();
    
    // Define a custom matcher logic
    const isEven = matches((val: number) => val % 2 === 0);
    
    myMock(4);
    expect(myMock).toHaveBeenCalledWith(isEven());
  12. Capture arguments using captor()

    main

    The captor() function creates a CaptorMatcher which acts as an asymmetric matcher but also stores the values it matches. This is useful when you need to inspect the exact arguments passed to a mock after the assertion has been made.

    When a CaptorMatcher is used in an expectation, it populates its value property (the most recent match) and its values array (all matches).

    Note: The value property is updated every time the matcher is triggered during the assertion process.

    import { captor } from 'vitest-mock-extended';
    import { expect, vi }
    
    const myMock = vi.fn();
    const myCaptor = captor<string>();
    
    myMock('first');
    myMock('second');
    
    // Use the captor in an expectation
    expect(myMock).toHaveBeenCalledWith(myCaptor());
    
    // Access the captured values
    console.log(myCaptor.value);   // 'second'
    console.log(myCaptor.values); // ['first', 'second']