0.0.12 • Published 9 months ago

mock-typed v0.0.12

Weekly downloads
-
License
MIT
Repository
github
Last release
9 months ago

mock-typed

A tiny TypeScript library for type-safe function mocks in unit tests

Installation

Depending on the used package manager, one of the following:

npm install -D mock-typed
yarn add -D mock-typed

Usage

Package mock-typed contains test mock helpers that help write strictly type-safe, straightforward, and maintainable code with full support of IDEs.

☝🏼 Regardless of which testing framework is used, jest or vi, safe and flexible mocking is crucial.

mock.returnValue(fn | mock<fn>, value[, options])

Sets the mocked function to return the provided value.

The value argument is type-safe but deeply partial. This means you can provide props that are only required for the test. Also, during coding, you get early typescript errors.

Before:

import { useUser } from 'hooks'; 

jest.mock('hooks/useUser');

// funky but still:
const useUserMock = useUser as jest.Mock;
// or better:
const useUserMock = jest.mocked(useUser);

// here, weird things are starting to happen
// for some reason, we get the module { useUser: [Function] }
// however, the type of useUserActual is `any`
const useUserActual = jest.requireActual('hooks/useUser')

// Yeah, useUserMock is typed as jest.MockedFunctionDeep<fn>, but...
useUserMock.mockImplementation(() => ({
  // useUserActual is any. Therefore, the type of
  // the whole thing is collapsed to any *:
  ...useUserActual,
  // As a result, the developer gets no cue that they write
  // unsafe code. F.x., a real case: there is no such property
  // in the return type of useUser:
  channels: ['GA', 'ANDR'],
}));

No brainer, this mock code silently gets broken when the API changes.

After:

import { mock } from 'mock-typed';
import { useUser } from 'hooks'; 

jest.mock('hooks/useUser');

// either useUser or useUserMock can be passed:
mock.returnValue(useUser, {
  // thankfully to the type safety, we found the error,
  // but maintain the flexibility
  tenant: {
    // providing only the required fields
    channels: ['GA', 'ANDR'],
  },
});

When you write something wrong you get a ts error.

Autocomplete in IDE also works.

mock.impl(fn | mock<fn>, implementation[, options])

Sets the mock implementation maintaining type safety of the mock function return type.

type Fn = <Code extends number>(code: Code) => Result<Code>;

type Result<Code extends number> = {
  code: Code;
  status: string;
  validate: (value: string) => boolean;
  retry: () => void;
};

// suppose, the function to mock is imported
declare const fn: Fn;

// declare in the test suite
const result: MockValueMockedInput<Fn> = {
  validate: jest.fn(),
};

// set the implementation in `beforeEach`:
beforeEach(() => {
  // need to reset mocks to avoid side effects with
  // the `validate` method mock
  jest.resetAllMocks();

  // set the implementation
  mock.impl(fn, (code) => ({
    // spreading immutable props from the static object
    ...result,
    // setting the dynamic prop
    code,
    // omitting irrelevant props
  }));
});

// the `validate` method has the respective signature.
// result has a deeply partial object type, therefore,
// all nested members are optional, which requires `?.`
// type assertion:
result.validate?.('value');

expect(result.validate).toHaveBeenCalled();

// at the same time, it has Mock typing:
result.validate?.mockClear();

Advanced Usage

prepare callback

See the example in the test suite "when testing a demo component using MockValueMockedInput"`.

Suppose, you need to mock the result of a hook, f.x. useSub, which returns an object with nested properties.

  1. you provide a return value of the useSub;
  2. in the prepare callback, you get the object and do additional preparations: set the return value of lastMockedResult.events.subscribe method.
mock.impl(
  // jest/vi has mocked the hook in the module,
  // so here we have a Mock function
  hooks.useSub,
  // provide a mock implementation
  () => ({
    // dispatch method is omitted: no need to mock it
    // because the observer is called in the test
    sink: jest.fn(),
    events: {
      subscribe: jest.fn(),
      aa: 1,
    },
  }),
  {
    prepare: (result) => {
      // save the last result of our mock above.
      // cast to mocked result because we know it is
      lastMockedResult = result as typeof lastMockedResult;
      // provide nested mocks. it is better to do it here for
      // type safety and brevity
      mock.returnValue(lastMockedResult.events?.subscribe!, {
        unsubscribe: jest.fn(),
      });
      return result;
    },
  }
);
MockValueMockedInput type for value declarations

The helper type MockValueMockedInput provides a simple way to declare type-safe mock objects with jest/vi mock typing:

/** Some function's return type */
type Something = {
  create: {
    mutate: (id: number) => number;
  };
  clear: () => void;
};

/** A function to mock */
type FN = () => Something;

/** Mocked function result */
type MT = MockValueMockedInput<FN>;

let mt: MT;

// methods are respecting the signature
mt.create!.mutate!(1);

// and adding mock functionality
mt.create!.mutate!.mockReset!();
mt.clear!.mockClear();

Note: the type assertion: mt.create!.mutate!, which is required because the properties are optional.

Contribution

Add your feature or fix and create a Pull Request.

0.0.10

9 months ago

0.0.11

9 months ago

0.0.12

9 months ago

0.0.8

9 months ago

0.0.5

9 months ago

0.0.4

9 months ago

0.0.7

9 months ago

0.0.3

9 months ago

0.0.2

9 months ago

0.0.1

9 months ago