Skip to content

matthieugicquel/shm

Repository files navigation

shm

Simple http mocking, with good developer experience


yarn add --dev @matthieug/shm
Platform Status Notes
node / jest node>=18 required
node / jest / jsdom Polyfills required
node / vitest node>=18 required
bun with bun test ⚠️ test won't fail with afterEach(expectRequestsToMatchHandlers)
expo Install react-native-url-polyfill if using SDK < 50
react-native Install react-native-url-polyfill
browser

Basic usage

// Some setup/global file
import { installInterceptor } from "@matthieug/shm";

installInterceptor();

// `mockServer.ts` or equivalent
import { createMockServer } from "@matthieug/shm";

export const mockServer = createMockServer("https://test.com");

// Mock a request -- short syntax for the 90% case
mockServer.get<BodyType>("some-route", body);

// Or full syntax for more control
mockServer.get<BodyType>("item/:id", {
  request: { // a request must contain **at least** the specified items to match
    pathParams: { id: "12" } // match path params
    searchParams: { lang: "fr" } // match search params
    headers: { Authorization: "Bearer some-token" } // match headers
  },
  response: {
    body: { message: "here is your mock" }, // specify a json or string body
    status: 418 // specify a status code, default is 200
  }
});

// All usual http methods are available

Have a look at the type definitions for more details.

Important notes:

  • Handlers will by default only respond to ONE matching request. After that, they will be "consumed"
  • Handlers are used in a first_in_first_out order

Usage in tests

Setup

// `jest-setupAfterEnv.js` or equivalent
import { installInterceptor, expectRequestsToMatchHandlers } from "@matthieug/shm";

// Prevent all outgoing requests -- Unhandled requests will be responded to with a 404
installInterceptor();

// Fail tests when there are unhandled requests or unused handlers, and clear handlers
afterEach(expectRequestsToMatchHandlers);
// `mockServer.ts` or equivalent
import { createMockServer } from "@matthieug/shm";

export const mockServer = createMockServer("https://test.com");

Ensure good DX with expectRequestsToMatchHandlers

Using it in your tests will:

  • keep tests isolated, by resetting the mock handlers
  • enforce removal of the unused handlers that could creep up as your code evolves, by throwing an error if a handler was not called
  • ensure your tests do not pass "by coincidence" and help with debugging issues, by throwing an error if a request was not handled
import { expectRequestsToMatchHandlers } from "@matthieug/shm";

afterEach(expectRequestsToMatchHandlers);

test("some test", async () => {
  mockServer.get("hello", body);
  await fetch("https://test.com/hallo");
});

// SHM: Received requests did not match defined handlers
//   UNHANDLED REQUEST: GET https://test.com/hello
//       --> handler GET https://test.com/hallo -> url /hallo !== /hello

Check that an API call was made with mockHandler.wasCalled

expect(mockHandler.wasCalled()).toBe(true);

This can be useful even if you're using the recommended setup with expectRequestsToMatchHandlers, eg to:

  • make the expectation explicit ("when user clicks this, my app should save that to the backend")
  • wait for the call to have been made
  • check that the call was made at the right time in a multi-step tests

Check that the correct request body was sent with mockHandler.getSentBody

test("my test", async () => {
  const mockHandler = mockServer.post("item", "here is your mock");

  await fetch("https://test.com/item", { method: "POST", body: "here's my request" });

  expect(await mockHandler.getSentBody()).toEqual("here's my request");
});

What's different from other http mocking libraries?

There are great alternatives out there, like msw or nock. By the way this package is using @mswjs/interceptors under the hood in node and the browser.

We want to promote a certain way to define and use api mocks in tests, and provide a very simple API.

  • Enforce maintenance of API mocks, by failing tests on unhandled requests and unused handlers
  • No way to write complex request matchers. Tests should avoid conditionnals, and this principle includes your mock definitions (otherwise you should write tests for your tests 🤔)
  • Check that your code is sending the correct request through assertions, instead of by coincidentally definining the right handler
  • Prefer specifying the necessary mocks for each test, so that you know at a glance what APIs your feature/component needs

Usage in an app

All the basic APIs are available, but there are a few specific options that you may want to use in this case:

import {
  installInterceptor,
  createMockServer,
  uninstallInterceptor,
  passthrough,
} from "@matthieug/shm";

// Start intercepting -- let unhandled requests passthrough
installInterceptor({ onUnhandled: passthrough });

const mockServer = createMockServer("https://test.com", {
  // options specified here will apply to all handlers
  delayMs: 500, // view your loading states
  persistent: true, // allow handlers to respond to multiple matching requests
});

// When you want to make real requests again
uninstallInterceptor();

Future plans

I don't plan to add lots of features, but I strive for very high quality. Don't hesitate to open an issue if you find a bug, or if the docs are unclear, or if an error message is not helpful.