npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

hermes-test

v0.2.4

Published

26-64x faster than Jest. A test runner built for React Native and Expo. One esbuild pass, one process, zero Babel.

Readme

hermes-test

26–64x faster than Jest. A test runner built for React Native and Expo. One esbuild pass, one process, zero Babel — results in under a second.

1472 tests — 0.84s (cached)  |  5s with coverage

⚠️ Early release (v0). hermes-test is battle-tested on a production Expo app (1472 tests, 100% pass rate) but the API may still change.

Recommended approach: Use .hermes.test.ts as your test file suffix. This lets you run hermes-test alongside Jest without overwriting your existing tests. Migrate one file at a time, verify it passes in both runners, then expand. Don't delete your Jest tests until you're confident.


The problem

Jest in React Native is slow by design. Every test file spawns a worker, runs Babel transforms, resolves transformIgnorePatterns for every node_modules import, and coordinates results over IPC. For a mid-size Expo app, that's 1-2 minutes per run. With coverage, even longer.

On top of that, the configuration tax is real: transformIgnorePatterns breaks every time you add a dependency, jest-expo mocks silently drift from real APIs, and moduleNameMapper requires manual upkeep for every monorepo alias. Developers stop running tests. Tests rot. Coverage drops.

The fix

hermes-test replaces the entire Jest pipeline with two things: esbuild (one bundle pass, <100ms) and a Rust CLI that evaluates it in a single process. No workers, no Babel, no transformIgnorePatterns. Native modules are auto-detected and externalized — zero manual configuration needed.

Your tests run in Hermes — the same JavaScript engine your app ships with — so you also get engine parity for free. But the real win is speed: results appear before your hand leaves Cmd+S.

Benchmarks

Production Expo app (Topdanmark, Danish insurance — 259 files, 1472 tests):

| | Jest | hermes-test | Speedup | |---|---|---|---| | Full suite (no coverage) | 54s | 0.84s cached / 2.5s cold | 64x / 22x | | Full suite (with coverage) | 128s | 5s | 26x | | Watch rerun | ~3s | ~300ms | 10x |

Micro benchmarks (Apple Silicon, no coverage):

| Scenario | hermes-test | Jest + @swc/jest | Speedup | |----------|-------------|------------------|---------| | 10 pure function tests | 16ms | 714ms | 45x | | 50 hook tests (renderHook + act) | 75ms | 721ms | 10x | | Trivial cold start | 4.6ms | 1,486ms | 364x |


Quick start

bun add -D hermes-test
// useCounter.test.ts
import { test, expect, renderHook, act } from 'hermes-test';

test('useCounter increments', () => {
  const { result } = renderHook(() => useCounter(0));
  act(() => result.current.increment());
  expect(result.current.count).toBe(1);
});
hermes-test              # run all tests
hermes-test --watch      # watch mode

API

Test structure

import { test, expect, group, beforeEach, afterEach } from 'hermes-test';

group('myFeature', () => {
  beforeEach(() => { /* reset */ });

  test('does the thing', () => {
    expect(result).toBe(42);
    expect(arr).toEqual([1, 2, 3]);
    expect(str).toContain('hello');
    expect(fn).toThrow('error message');
  });

  test.skip('not yet', () => {});
  test.only('focus this', () => {});
  test('slow test', () => { /* ... */ }, { timeout: 10000 });
});

Assertions

expect(val).toBe(exact)            expect(val).toEqual(deep)
expect(val).toBeTruthy()           expect(val).toBeFalsy()
expect(val).toBeDefined()          expect(val).toBeUndefined()
expect(val).toBeNull()             expect(val).toBeGreaterThan(n)
expect(val).toContain(item)        expect(val).toContainEqual(item)
expect(val).toMatch(/regex/)       expect(val).toBeCloseTo(n, precision)
expect(fn).toThrow('msg')          expect(val).not.toBe(other)

// Asymmetric matchers
expect.anything()                  expect.any(String)
expect.objectContaining({ key })   expect.arrayContaining([1, 2])
expect.stringContaining('sub')     expect.stringMatching(/pattern/)

// Async
await expect(promise).resolves.toBe(value)
await expect(promise).rejects.toThrow('msg')

Spies

import { spy, spyOn, clearAllMocks } from 'hermes-test';

const fn = spy(() => 'default');
fn.mockReturnValue('mocked');
fn.mockReturnValueOnce('first');
fn.mockImplementation((x) => x * 2);
fn.mockResolvedValue('async');

expect(fn).toHaveBeenCalled();
expect(fn).toHaveBeenCalledWith('arg1', 'arg2');
expect(fn).toHaveBeenCalledTimes(3);
expect(fn.calls[0][0]).toBe('arg1');   // direct access

// spyOn — intercept real object methods
const s = spyOn(storage, 'get');
s.mockReturnValue('cached');
s.mockRestore();   // revert to original

// Clear all spies at once
clearAllMocks();

Module mocking

import { mockModule } from 'hermes-test';
import { useMyHook } from './useMyHook';  // import order doesn't matter

mockModule('./useRedux', () => ({
  useAppSelector: (selector) => mockState,
}));

Shadow wrappers check mocks at call time — mockModule can appear before or after imports.

Hook testing

import { renderHook, act, waitFor } from 'hermes-test';

const { result, history, renderCount } = renderHook(() => useCounter(0));
act(() => result.current.increment());
expect(result.current.count).toBe(1);
expect(renderCount).toBe(2);

Fetch mocking (MSW-style)

import { mockFetch, mockFetchUse, mockFetchReset, http, HttpResponse } from 'hermes-test';

mockFetch(
  http.get('https://api.example.com/data', () => HttpResponse.json({ ok: true })),
  http.post('https://api.example.com/login', () => HttpResponse.json({ token: '...' })),
);

// Per-test override
mockFetchUse(http.get('https://api.example.com/data', () => HttpResponse.error()));

// Cleanup
mockFetchReset();

Redux store

import { setupApiStore } from 'hermes-test/store';

const ctx = setupApiStore([api, cms], { app: rootReducer }, {
  preloadedState: { app: { auth: { session: mockSession } } },
});
const { result } = ctx.renderHookWithReduxStore(() => useMyHook());
ctx.store.dispatch(authActions.logout());

Fake timers

import { useFakeTimers, advanceTimersByTime, useRealTimers } from 'hermes-test';

useFakeTimers();
setTimeout(() => { fired = true }, 1000);
advanceTimersByTime(1000);
expect(fired).toBe(true);
useRealTimers();

How it works

┌──────────────┐     ┌─────────┐     ┌────────────┐
│  .test.ts    │────▶│ esbuild │────▶│   Hermes   │
│  files       │     │ bundle  │     │   VM eval  │
└──────────────┘     └─────────┘     └────────────┘
       │                  │                 │
  mockModule()      <100ms bundle     native execution
  spy/expect        path aliases      drainMicrotasks
  renderHook        Hermes patches     real React tree
  1. esbuild bundles your test + source into a single IIFE (~100ms)
  2. Rust CLI applies Hermes patches (class-extends, for-let-of)
  3. Bytecode compilation — cached .hbc for instant loading on subsequent runs
  4. Hermes VM evaluates the bytecode — same engine as your app
  5. Results printed to terminal — single process, no workers, no IPC

Three-tier cache

| Tier | What | Speed | |---|---|---| | Bytecode (.hbc) | Pre-compiled Hermes bytecode | Fastest — skip JS parsing | | Patched JS | Post-patched esbuild output | Fast — skip bundling + patching | | Fresh bundle | Full esbuild + patch pipeline | Cold start only |

Auto-detect native externals

Native modules are detected automatically by scanning node_modules for ios/, android/, *.podspec, and app.plugin.js. No manual externals config needed for standard React Native packages.

Mock isolation (Shadow Wrappers)

When multiple test files mock the same module differently, hermes-test uses shadow wrappers — filesystem-based Proxy wrappers that check which test file is running at call time. One bundle, one runtime, per-file mock isolation.

CLI

hermes-test                          # run all test files
hermes-test src/hooks/               # run tests in a directory
hermes-test src/hooks/useLogin.test.ts  # run a specific file
hermes-test --watch                  # watch mode — reruns on file changes
hermes-test --watch useLogin         # watch mode, filtered to matching files
hermes-test --coverage               # run with coverage (lcov + HTML report)

Configuration

Polyrepo (single package)

No config file needed for simple projects. Just run hermes-test in your project root.

my-app/
├── src/
│   └── hooks/
│       └── useLogin.hermes.test.ts
├── package.json
└── tsconfig.json          ← path aliases read automatically

Monorepo

Create hermes-test.config.json in your app directory. The root field tells hermes-test where the monorepo root is (for resolving shared node_modules).

monorepo/
├── apps/
│   └── my-app/
│       ├── src/
│       ├── hermes-test.config.json   ← config here
│       ├── package.json
│       └── tsconfig.json
├── packages/
│   └── shared/
└── node_modules/                     ← root points here
{
  "root": "../..",
  "testMatch": ".hermes.test.ts"
}

hermes-test.config.json

| Key | Description | Required | |-----|-------------|----------| | root | Monorepo workspace root (for resolving node_modules) | Monorepo only | | testMatch | Test file suffix (default: .test.ts) | No | | externals | Additional modules to externalize | No (most auto-detected) | | shims | Built-in or custom module replacements | No | | split | Enable vendor/group bundle splitting for large suites | No | | coverageThreshold | Minimum coverage % — fails if below (e.g. 65) | No |

tsconfig paths are read automatically — monorepo path aliases just work:

{
  "compilerOptions": {
    "paths": {
      "@app/*": ["./src/*"],
      "@myorg/shared/*": ["../../packages/shared/src/*"]
    }
  }
}

Native externals are auto-detected by scanning node_modules for ios/, android/, *.podspec, and app.plugin.js. Most projects need zero manual externals.

Built-in shims

hermes-test ships with ready-to-use shims for common React Native ecosystem packages. Use hermes-test/shims/<name> in your config — no local shim files needed.

| Shim | What it provides | |------|-----------------| | hermes-test/shims/react-native | Platform, StyleSheet, Dimensions, Alert, Linking stubs | | hermes-test/shims/react-i18next | Identity translation (t('key') returns 'key') | | hermes-test/shims/async-storage | In-memory AsyncStorage (getItem, setItem, clear, etc.) | | hermes-test/shims/rtk-query | RTK Query createApi singleton cache | | hermes-test/shims/react-redux | Pass-through for react-redux | | hermes-test/shims/reduxjs-toolkit | Pass-through for @reduxjs/toolkit |

Example config with shims:

{
  "root": "../..",
  "testMatch": ".hermes.test.ts",
  "shims": {
    "react-i18next": "hermes-test/shims/react-i18next",
    "@reduxjs/toolkit/query/react": "hermes-test/shims/rtk-query",
    "@react-native-async-storage/async-storage": "hermes-test/shims/async-storage"
  }
}

You can also write custom shims for app-specific native modules:

{
  "shims": {
    "react-native-keychain": "./test/shims/keychain.js"
  }
}

Coverage

hermes-test --coverage

Generates:

  • Terminal table — per-file line + function coverage with color coding
  • coverage/lcov.info — standard lcov format, works with any lcov tool
  • coverage/index.html — interactive HTML report with source-level green/red highlighting

Coverage uses esbuild source maps for accurate original-file line mapping. Imports, node_modules, test files, and monorepo dependencies are automatically excluded — only your source code is measured.

Coverage threshold

Add coverageThreshold to hermes-test.config.json to fail CI when coverage drops:

{
  "coverageThreshold": 65
}

If total statement coverage is below the threshold, hermes-test exits with code 1.

Stack

  • Hermes — the JS engine that ships with React Native and Expo
  • esbuild — bundler, 100x faster than Babel/Metro transforms
  • Rust — CLI host, native Hermes FFI, bytecode caching
  • TypeScript — test harness (spy, expect, renderHook, mockFetch, timers)

Why not Jest?

| | Jest + jest-expo | hermes-test | |---|-----------------|-------------| | Bundling | Babel on every import | esbuild, one pass | | Startup | ~700ms per worker | ~5ms total | | Native externals | Manual transformIgnorePatterns | Auto-detected | | Config needed | transformIgnorePatterns, moduleNameMapper, mocks | Zero for most projects | | Watch rerun | ~2-3s | ~300ms | | 1472 tests (no coverage) | 54s | 0.84s | | 1472 tests (with coverage) | 128s | 5s | | Coverage | Built-in (v8/Istanbul) | --coverage with source maps, HTML report, threshold | | Engine | Node | Hermes (same as your app) |

Platform support

| Platform | Status | |----------|--------| | macOS (Apple Silicon) | Supported | | Linux (x64) | Supported | | macOS (Intel x64) | Planned | | Windows | Not planned |

Roadmap

  • [x] Coverage reporting — source map-based instrumentation, lcov + HTML report, threshold enforcement
  • [ ] macOS Intel (x64) — cross-compile or dedicated CI runner
  • [ ] Component renderingrender(<Component />) with query API (getByText, getByTestId, fireEvent)
  • [ ] Jest compatibility shimjest.fn()spy(), jest.mock()mockModule(), enables reuse of library __mocks__/ files
  • [ ] Library mock support — auto-load mocks from expo-router, react-native-reanimated, zustand, etc.
  • [ ] setupFiles config — load setup files before tests (like Jest's setupFilesAfterFramework)

License

MIT