ApiaryActive
Try: pause · settings · learn · wipe
← Community / Reading Room
UT
craft · 14 min read

Unit Testing with Jest

Unit testing is the foundation of reliable software. In the world of Apiary, where we build self‑governing AI agents that help protect pollinator populations,…

Unit testing is the foundation of reliable software. In the world of Apiary, where we build self‑governing AI agents that help protect pollinator populations, even a single misbehaving function can cascade into a decision that jeopardises a hive. Jest gives us a robust, fast, and expressive way to guard against such regressions. It’s not just a testing library; it’s an ecosystem that integrates with Babel, TypeScript, React, and even native modules. By mastering Jest’s mocking, spying, and snapshot capabilities, you can write test suites that are both exhaustive and maintainable, turning every line of code into a contract that the rest of the system can rely on.

The power of Jest lies in its simplicity: a single command (npm test) runs thousands of tests, reports coverage, and can be configured to run in parallel or watch mode. Yet beneath that surface are sophisticated mechanisms that let you simulate complex dependencies, assert that side‑effects happened as expected, and freeze the rendered output of UI components for visual regression. In this pillar article we’ll dive deep into these mechanisms, walking through concrete examples that range from a pure‑function test to a full‑stack scenario that mocks network calls, timers, and even a virtual bee hive. By the end you’ll have a practical playbook for writing comprehensive test suites that keep your conservation projects humming.


1. Getting Started with Jest

Installation and Configuration

Jest ships with zero configuration for most Node.js projects, but a few knobs can dramatically improve the developer experience. In a typical Apiary repo you’ll see:

# Add Jest and its TypeScript support
npm install --save-dev jest @types/jest ts-jest

# Add a script to package.json
"scripts": {
  "test": "jest --coverage"
}

The ts-jest preset tells Jest to compile TypeScript on the fly, while --coverage outputs a coverage report after each run. A minimal jest.config.js might look like:

module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  collectCoverageFrom: [
    '**/src/**/*.ts',
    '!**/src/**/*.d.ts',
    '!**/src/**/index.ts',
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 90,
      lines: 95,
      statements: 95,
    },
  },
};

This config tells Jest to run tests in a Node environment, collect coverage from all TypeScript files except declaration files and index hubs, and enforce strict thresholds. The thresholds are a good starting point; adjust them based on your project’s maturity and risk tolerance. In a conservation‑oriented codebase, a 95 % line coverage threshold ensures that almost every path a bee‑sensing algorithm could take is exercised.

Test File Structure

Jest automatically discovers test files that match *.test.ts or *.spec.ts. Organising tests alongside source files keeps them in sync:

src/
  utils/
    math.ts
    math.test.ts
  services/
    beeSensor.ts
    beeSensor.test.ts

This layout mirrors the dependency graph: tests live next to the code they verify, making it easier to refactor without breaking tests.


2. Writing Your First Test Suite

Let’s start with a simple utility function that calculates the area of a circular flower patch—a common operation in our bee‑tracking algorithms.

// src/utils/geometry.ts
export function circleArea(radius: number): number {
  return Math.PI * radius * radius;
}

A test for this function is straightforward:

// src/utils/geometry.test.ts
import { circleArea } from './geometry';

describe('circleArea', () => {
  it('returns the correct area for a given radius', () => {
    const radius = 5;
    const area = circleArea(radius);
    expect(area).toBeCloseTo(78.5398, 4);
  });

  it('handles zero radius', () => {
    expect(circleArea(0)).toBe(0);
  });

  it('throws if radius is negative', () => {
    expect(() => circleArea(-3)).toThrow('Radius must be non‑negative');
  });
});

A few key points:

  • describe blocks group related tests, mirroring the function’s responsibilities.
  • it blocks describe individual behaviours. The phrasing “returns the correct area for a given radius” is a contract that other developers can read and understand without diving into implementation details.
  • toBeCloseTo is Jest’s way of asserting floating‑point equality with a specified precision, avoiding flaky tests due to binary rounding.

Run the test with npm test. You’ll see a concise output and a coverage report that shows 100 % coverage for circleArea. This is a simple example, but the pattern scales to large modules, asynchronous functions, and side‑effect‑heavy logic.


3. Mocking Dependencies

In a real‑world Apiary project, functions rarely operate in isolation. They depend on external services (e.g., weather APIs), database layers, or hardware sensors. Jest’s mocking system lets you replace these dependencies with controlled stand‑ins, ensuring deterministic tests.

Manual vs. Auto Mocks

// src/services/beeSensor.ts
export async function fetchBeeCount(apiKey: string) {
  const response = await fetch(`https://api.beeapi.com/count?key=${apiKey}`);
  const data = await response.json();
  return data.count;
}

A naive test that hits the real API would be flaky and slow. Instead, we mock the fetch global:

// src/services/beeSensor.test.ts
global.fetch = jest.fn();

describe('fetchBeeCount', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('returns the count from the API', async () => {
    const mockResponse = {
      ok: true,
      json: async () => ({ count: 42 }),
    };
    (fetch as jest.Mock).mockResolvedValue(mockResponse);

    const count = await fetchBeeCount('dummy-key');
    expect(count).toBe(42);
    expect(fetch).toHaveBeenCalledWith(
      'https://api.beeapi.com/count?key=dummy-key'
    );
  });
});

Here, we:

  • Replace fetch with a Jest mock.
  • Return a fake response that mimics the real API contract.
  • Assert that the function called fetch with the correct URL.

For larger modules, Jest’s auto‑mocking feature can automatically replace entire files:

jest.mock('../db', () => ({
  getBeeHive: jest.fn().mockResolvedValue({ id: 1, bees: 120 }),
}));

This is useful when you want to mock an entire database layer without writing boilerplate for each method.

Mocking External Libraries

Sometimes you rely on third‑party libraries that you don’t control. For example, suppose you use axios to fetch weather data. Jest can mock the whole library:

jest.mock('axios');
import axios from 'axios';

(axios.get as jest.Mock).mockResolvedValue({
  data: { temperature: 23, humidity: 60 },
});

Because axios is a CommonJS module, Jest’s auto‑mock will replace its exported functions with Jest mock functions, which you can then configure per test.


4. Spies and Assertions

While mocks replace dependencies, spies let you observe how a function is used. Spies are invaluable for verifying side‑effects, such as logging, event emission, or callback invocation.

Using jest.spyOn

// src/services/logger.ts
export function log(message: string) {
  console.log(`[BeeMonitor] ${message}`);
}
// src/services/beeMonitor.test.ts
import * as logger from '../services/logger';
import { monitorBeeHive } from './beeMonitor';

describe('monitorBeeHive', () => {
  it('logs when the hive is healthy', () => {
    const spy = jest.spyOn(logger, 'log').mockImplementation(() => {});
    monitorBeeHive({ bees: 150 });
    expect(spy).toHaveBeenCalledWith('Hive is healthy');
    spy.mockRestore();
  });
});

The spy records every call to log, enabling assertions about message content, call count, and order. After the test, mockRestore() returns the original implementation, preventing side‑effects on other tests.

Mock Implementations

Spies can also replace a function’s implementation temporarily:

jest.spyOn(logger, 'log').mockImplementation((msg) => {
  /* swallow logs during tests */
});

This is handy when you want to silence noisy output but still verify that the function was called.

Verifying Call Order

When multiple callbacks are involved, the order matters. Jest provides toHaveBeenCalledTimes and toHaveBeenNthCalledWith:

const callback = jest.fn();
doSomething(callback);
expect(callback).toHaveBeenNthCalledWith(1, 'first');
expect(callback).toHaveBeenNthCalledWith(2, 'second');

This ensures that the algorithm’s control flow is exactly as intended.


5. Snapshot Testing

Snapshot tests capture the rendered output of a component or the stringified result of a function, and compare it against a stored snapshot. They are especially useful for UI components, but they can also guard against unintended changes in data structures.

Snapshot Basics

// src/components/Flower.tsx
export const Flower = ({ color }: { color: string }) => (
  <div className={`flower ${color}`}>🌸</div>
);
// src/components/Flower.test.tsx
import { render } from '@testing-library/react';
import { Flower } from './Flower';

test('renders correctly', () => {
  const { container } = render(<Flower color="yellow" />);
  expect(container).toMatchSnapshot();
});

The first run creates Flower.test.tsx.snap:

exports[`renders correctly 1`] = `
<div>
  <div
    class="flower yellow"
  >
    🌸
  </div>
</div>
`;

Subsequent runs compare the rendered output to this snapshot. If the component changes, the test fails, prompting you to either update the snapshot (if the change is intentional) or fix the code.

Snapshot Best Practices

  1. Granularity: Snapshots should be small. If a component renders a large table, consider snapshotting only the critical parts or the data payload instead of the entire DOM.
  2. Selective Updates: Use jest --updateSnapshot to refresh only the snapshots that have changed. Review changes before committing.
  3. Avoid Overuse: Snapshots are great for visual regression but not a replacement for explicit assertions. Combine them with expect statements to test specific values.
  4. Custom Serializers: For complex objects like dates or circular references, register a serializer:
   import serializer from 'jest-serializer-path';
   jest.addSnapshotSerializer(serializer);
  1. Snapshot Size: Keep snapshots under 1 KB. Large snapshots become difficult to review and can hide subtle regressions.

Snapshotting Data Structures

Snapshot tests are not limited to UI. Suppose you have a function that returns a bee‑colony health report:

export function generateReport(hive: Hive) {
  return {
    hiveId: hive.id,
    beeCount: hive.bees.length,
    health: calculateHealth(hive),
  };
}

A snapshot test ensures that the report format stays consistent:

test('generateReport snapshot', () => {
  const hive = { id: 1, bees: Array(120).fill({ id: 42 }) };
  const report = generateReport(hive);
  expect(report).toMatchSnapshot();
});

If the report structure changes (e.g., you add a temperature field), the snapshot test will alert you, preventing downstream consumers from breaking.


6. Advanced Mocking Patterns

Mocking is powerful, but the real world throws in timers, asynchronous flows, and nested dependencies. Jest provides a suite of utilities to handle these scenarios.

Mocking Timers

When your code uses setTimeout, setInterval, or Date.now, you can control time deterministically:

jest.useFakeTimers();

test('debounced function', () => {
  const debounced = debounce(() => console.log('boom'), 300);
  debounced(); // schedule
  jest.advanceTimersByTime(299);
  expect(console.log).not.toHaveBeenCalled();
  jest.advanceTimersByTime(1);
  expect(console.log).toHaveBeenCalledWith('boom');
});

jest.useRealTimers() restores normal behavior after the test.

Mocking Async Modules

Suppose you have an async function that fetches data and then processes it:

export async function processWeather() {
  const { temperature } = await fetchWeather();
  return analyze(temperature);
}

You can mock fetchWeather with a promise that resolves after a delay:

jest.mock('./weather', () => ({
  fetchWeather: jest.fn().mockResolvedValue({ temperature: 18 }),
}));

If you need to simulate a network error:

(fetchWeather as jest.Mock).mockRejectedValue(new Error('Network'));

Mocking Nested Dependencies

When a module imports another module that itself imports a third module, Jest’s module registry keeps a single copy of each module. If you mock a dependency in a test, the mock propagates to all consumers. This can be a double‑edged sword: it simplifies mocking but can lead to unexpected shared state. Use jest.resetModules() between tests to isolate the module registry.

afterEach(() => {
  jest.resetModules();
});

Mocking Global Objects

Sometimes you need to mock global objects like window.localStorage or navigator.geolocation. Jest lets you replace them:

Object.defineProperty(window, 'localStorage', {
  value: {
    getItem: jest.fn(),
    setItem: jest.fn(),
  },
});

This is useful for testing client‑side logic that persists state across sessions.


7. Testing React Components with Jest

React components are the visual glue of an Apiary dashboard. While snapshot tests guard against UI regressions, interaction tests ensure that user events behave correctly. The React Testing Library (RTL) pairs naturally with Jest.

Rendering and Querying

import { render, screen, fireEvent } from '@testing-library/react';
import { BeeCounter } from './BeeCounter';

test('increments count on click', () => {
  render(<BeeCounter initial={10} />);
  const button = screen.getByRole('button', { name: /increment/i });
  fireEvent.click(button);
  expect(screen.getByText(/10 bees/i)).toBeInTheDocument();
});

RTL encourages querying elements the way a user would: by text, role, or label. This reduces brittle selectors.

Testing Asynchronous Updates

If a component fetches data on mount:

test('shows loading then data', async () => {
  (fetchBeeCount as jest.Mock).mockResolvedValue(42);
  render(<BeeCounter />);
  expect(screen.getByText(/loading/i)).toBeInTheDocument();
  await waitFor(() => expect(screen.getByText(/42 bees/i)).toBeInTheDocument());
});

waitFor repeatedly checks a condition until it passes or a timeout occurs. Jest’s default timeout is 5 s; you can adjust it per test with jest.setTimeout.

Snapshotting Components

Combine RTL with snapshots to capture the component tree:

import { toJson } from 'enzyme-to-json';
import { shallow } from 'enzyme';

test('snapshot of BeeCounter', () => {
  const wrapper = shallow(<BeeCounter initial={10} />);
  expect(toJson(wrapper)).toMatchSnapshot();
});

This is useful when you’re using Enzyme and want to capture the component’s rendered output.

Testing Context Providers

Apiary’s UI uses React Context for global state (e.g., current hive selection). To test a component that consumes context:

import { HiveProvider, useHive } from '../context/HiveContext';

test('shows hive name', () => {
  const TestComponent = () => {
    const { hive } = useHive();
    return <div>{hive.name}</div>;
  };
  render(
    <HiveProvider value={{ hive: { name: 'Alpine Meadow' } }}>
      <TestComponent />
    </HiveProvider>
  );
  expect(screen.getByText(/Alpine Meadow/i)).toBeInTheDocument();
});

By providing a mock context value, you isolate the component from the global store.


8. CI/CD Integration

Unit tests should never be run only locally. In a conservation project, failing tests can mean delayed deployments that affect real‑world monitoring. Integrating Jest into CI pipelines ensures that every commit is validated.

GitHub Actions

name: CI

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm test -- --coverage
      - uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: coverage/lcov-report

This workflow installs dependencies, runs tests, collects coverage, and uploads the report as an artifact. You can also publish the coverage report to a static site for visual inspection.

Reporting Test Results

Jest can output results in JUnit XML format, which many CI systems understand:

npm install --save-dev jest-junit

Add to jest.config.js:

module.exports = {
  reporters: [
    'default',
    ['jest-junit', { outputDirectory: './reports', outputName: 'junit.xml' }],
  ],
};

The XML file can be consumed by dashboards or used to trigger alerts when test failures occur.

Parallelism and Caching

Jest can run tests in parallel across workers (--maxWorkers). In CI, you can limit workers to avoid exhausting CPU:

- run: npm test -- --maxWorkers=2

Cache node_modules and Jest’s cache directory to speed up builds:

- uses: actions/cache@v4
  with:
    path: |
      ~/.npm
      .jest-cache
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}

9. Debugging Test Failures

Even with a solid test suite, failures happen. Jest provides tools to pinpoint the root cause.

Watch Mode

npm test -- --watch

Watch mode reruns only affected tests after each file change. It’s ideal for local debugging.

Verbose Logging

Add console.log statements inside the test or the code under test. Jest preserves console output, making it easy to trace execution.

Detecting Open Handles

Sometimes async code leaves open timers or sockets, causing Jest to hang:

npm test -- --detectOpenHandles

This prints a stack trace of all open handles, helping you locate the culprit.

Running Tests Serially

Parallel tests can race on shared resources. Force serial execution:

npm test -- --runInBand

This can help diagnose flakiness due to shared state.

Snapshot Diffing

When a snapshot test fails, Jest shows a diff of the old vs. new snapshot. Pay attention to whitespace and formatting; often a stray newline can cause a failure.


10. Testing AI Agents and Conservation Models

Apiary’s flagship feature is a self‑governing AI agent that decides when to deploy drones, adjust irrigation, or alert beekeepers. Testing these agents is crucial because their decisions directly affect bee populations.

Mocking Sensors

An agent reads data from virtual sensors (temperature, humidity, pollen density). In tests, you replace these sensors with deterministic mocks:

jest.mock('../sensors/temperature', () => ({
  read: jest.fn().mockReturnValue(25),
}));

Now the agent’s logic receives a known temperature, enabling you to assert its decision path.

Simulating Bee Behavior

You can model a bee colony as a simple state machine and expose an API for the agent to query:

export interface BeeHive {
  id: number;
  bees: number;
  health: number; // 0-100
}

export function getHive(id: number): BeeHive {
  /* real implementation */
}

In tests:

jest.mock('../hives', () => ({
  getHive: jest.fn().mockImplementation((id) => ({
    id,
    bees: 120,
    health: 85,
  })),
}));

The agent then makes decisions based on this data. Assertions might look like:

test('agent deploys drone when health < 50', () => {
  (getHive as jest.Mock).mockReturnValue({ id: 1, bees: 80, health: 45 });
  const action = agent.decide(1);
  expect(action).toBe('deployDrone');
});

Testing Reinforcement Learning Loops

If the agent uses a reinforcement learning (RL) loop, you can mock the environment to provide deterministic rewards:

jest.mock('../env', () => ({
  step: jest.fn().mockImplementation((action) => ({
    reward: action === 'collectPollen' ? 10 : -5,
    done: false,
  })),
}));

Then test that the agent’s policy converges to the optimal action over a few simulated episodes.

Validation Against Conservation Metrics

Finally, you can assert that the agent’s decisions improve conservation metrics. For instance, after a simulated deployment:

expect(agent.metrics.beeSurvivalRate).toBeGreaterThan(0.9);

These tests ensure that the AI agent’s policy aligns with ecological goals, not just arbitrary reward functions.


Why It Matters

Unit testing with Jest is more than a development nicety; it’s a safety net for the ecosystems we’re building to protect. In Apiary, every line of code can influence decisions that affect thousands of bees, the health of pollinator corridors, and the sustainability of local agriculture. By mastering mocks, spies, and snapshot tests, you can:

  • Guarantee correctness: Detect regressions before they reach production, preventing costly mis‑deployments.
  • Improve confidence: Developers can refactor with the assurance that tests will catch unintended side‑effects.
  • Accelerate collaboration: Shared test contracts make onboarding new contributors smoother and reduce integration friction.
  • Support conservation metrics: Automated tests verify that AI agents meet ecological benchmarks, ensuring that technology serves the environment, not just the codebase.

In a world where software increasingly mediates the delicate balance of nature, a robust Jest test suite is a cornerstone of responsible, trustworthy engineering. By investing the time to write comprehensive tests today, you safeguard the future of bees, the integrity of your AI agents, and the health of the planet.

Frequently asked
What is Unit Testing with Jest about?
Unit testing is the foundation of reliable software. In the world of Apiary, where we build self‑governing AI agents that help protect pollinator populations,…
What should you know about installation and Configuration?
Jest ships with zero configuration for most Node.js projects, but a few knobs can dramatically improve the developer experience. In a typical Apiary repo you’ll see:
What should you know about test File Structure?
Jest automatically discovers test files that match *.test.ts or *.spec.ts . Organising tests alongside source files keeps them in sync:
What should you know about 2. Writing Your First Test Suite?
Let’s start with a simple utility function that calculates the area of a circular flower patch—a common operation in our bee‑tracking algorithms.
What should you know about 3. Mocking Dependencies?
In a real‑world Apiary project, functions rarely operate in isolation. They depend on external services (e.g., weather APIs), database layers, or hardware sensors. Jest’s mocking system lets you replace these dependencies with controlled stand‑ins, ensuring deterministic tests.
References & sources
  1. Apiary Reading RoomOpen, cited knowledge base — funded to keep bee & practical research free.
From the Apiary Reading Room. Opinion & editorial — not financial advice. We don't overclaim.
More from the Reading Room