Skip to main content
The UI extensions SDK provides comprehensive mocking support for testing extensions without making real API calls or requiring access to actual HubSpot data. You can mock extension context data, React hooks, extension actions, and serverless function calls. The mocking functionality is built on tinyspy, a minimal spy library. Each mock function is created using tinyspy’s spyOn function, which means all mock functions expose the standard tinyspy spy properties and methods for advanced testing scenarios.

Quick reference

Mock Types Available: Spy Methods:
  • nextResult(value): set the return value for the next single call only
  • willCall(fn): provide a custom implementation that persists across all calls
  • reset(): clear call history while keeping the mock implementation
  • restore(): restore to the default mock behavior
Spy Properties:
  • called: boolean indicating if called
  • callCount: number of times called
  • calls: array of argument arrays
  • returns: array of return values

Automatic mocks based on extension point location

When you create a renderer with createRenderer(extensionPointLocation), the testing SDK automatically creates appropriate mocks for the Extension Point API based on the location you specify. This includes:
  • context: Extension point-specific context data (user info, portal info, CRM data, variables, etc.)
  • actions: Extension point-specific actions (addAlert, reloadPage, fetchCrmObjectProperties, etc.)
  • runServerlessFunction(): A mock implementation for calling serverless functions
A renderer created with createRenderer('crm.record.tab') will provide different context and actions than one created with createRenderer('settings'). Always use the extension point location that matches where your component will be used.

Mocking React hooks

Default mock implementations

The testing SDK automatically provides default mock implementations for supported hooks. These defaults allow your components to render without additional configuration:
  • useCrmProperties(): Returns fake property values based on property names (e.g., firstnamefake_firstname)
  • useAssociations(): Returns a single fake association result with fake property values
import { createRenderer } from '@hubspot/ui-extensions/testing';
import { useCrmProperties } from '@hubspot/ui-extensions/crm';
import { Text } from '@hubspot/ui-extensions';

function MyComponent() {
  const { properties, isLoading, error } = useCrmProperties([
    'firstname',
    'lastname',
  ]);
  if (isLoading) {
    return <Text>Loading...</Text>;
  }
  if (error) {
    return <Text>Something went wrong!</Text>;
  }
  return (
    <>
      <Text>First name: {properties.firstname}</Text>
      <Text>Last name: {properties.lastname}</Text>
    </>
  );
}

const { render, findAll } = createRenderer('crm.record.tab');
render(<MyComponent />);

const textNodes = findAll(Text);
expect(textNodes[0].text).toEqual('First name: fake_firstname');
expect(textNodes[1].text).toEqual('Last name: fake_lastname');

Mocking the next result

Use the nextResult() method to mock the return value for the next single invocation of a hook. This is useful for testing specific states like loading or error conditions. The mock applies only to the next call and then reverts to the default behavior:
import { createRenderer } from '@hubspot/ui-extensions/testing';
import { useCrmProperties } from '@hubspot/ui-extensions/crm';
import { Text } from '@hubspot/ui-extensions';

function MyComponent() {
  const { properties, isLoading, error } = useCrmProperties([
    'firstname',
    'lastname',
  ]);
  if (isLoading) {
    return <Text>Loading...</Text>;
  }
  if (error) {
    return <Text>Something went wrong!</Text>;
  }
  return (
    <>
      <Text>First name: {properties.firstname}</Text>
      <Text>Last name: {properties.lastname}</Text>
    </>
  );
}

const {find, render, mocks } = createRenderer('crm.record.tab');

// Mock an error state
mocks.useCrmProperties.nextResult({
  properties: {},
  error: new Error('Something went wrong!'),
  isLoading: false,
});

render(<MyComponent />);
expect(find(Text).text).toEqual('Something went wrong!');

Custom mock functions

Use the willCall() method to provide a custom implementation for a hook that persists across all calls. This gives you full control over the hook’s behavior based on the input arguments. Unlike nextResult(), which applies to only the next call, willCall() replaces the mock implementation for all subsequent calls:
import { createRenderer } from '@hubspot/ui-extensions/testing';
import { useCrmProperties } from '@hubspot/ui-extensions/crm';
import { Text } from '@hubspot/ui-extensions';

function MyComponent() {
  const { properties, isLoading, error } = useCrmProperties([
    'firstname',
    'lastname',
  ]);
  if (isLoading) {
    return <Text>Loading...</Text>;
  }
  if (error) {
    return <Text>Something went wrong!</Text>;
  }
  return (
    <>
      <Text>First name: {properties.firstname}</Text>
      <Text>Last name: {properties.lastname}</Text>
    </>
  );
}

const { render, mocks, findAll } = createRenderer('crm.record.tab');

// Provide a custom mock implementation
mocks.useCrmProperties.willCall((propertyNames) => {
  const properties = propertyNames.reduce(
    (acc, propertyName) => {
      acc[propertyName] = propertyName.toUpperCase();
      return acc;
    },
    {} as Record<string, string>
  );

  return {
    properties,
    error: null,
    isLoading: false,
  };
});

render(<MyComponent />);

const textNodes = findAll(Text);
expect(textNodes[0].text).toEqual('First name: FIRSTNAME');
expect(textNodes[1].text).toEqual('Last name: LASTNAME');

Mocking useAssociations

The useAssociations() hook supports the same mocking methods as useCrmProperties():
import { createRenderer } from '@hubspot/ui-extensions/testing';
import { useAssociations } from '@hubspot/ui-extensions/crm';
import { Text } from '@hubspot/ui-extensions';

function MyComponent() {
  const { results, isLoading, error } = useAssociations({
    toObjectType: '0-1',
    properties: ['firstname', 'lastname'],
    pageLength: 10,
  });
  if (isLoading) {
    return <Text>Loading...</Text>;
  }
  if (error) {
    return <Text>Something went wrong!</Text>;
  }
  return (
    <>
      {results.map((result) => (
        <Text key={result.toObjectId}>
          {result.properties.firstname} {result.properties.lastname}
        </Text>
      ))}
    </>
  );
}

const { find, render, mocks } = createRenderer('crm.record.tab');

// Mock an error state for the next call only
mocks.useAssociations.nextResult({
  results: [],
  error: new Error('Something went wrong!'),
  isLoading: false,
});

render(<MyComponent />);
expect(find(Text).text).toEqual('Something went wrong!');

Mocking Extension Point API

The testing SDK automatically creates mocks for the Extension Point API based on the extension point location you provide to createRenderer(). These mocks allow you to test components that use context, actions, or runServerlessFunction() from the Extension Point API.

Mocking context

The context object is automatically populated with fake data appropriate for the extension point location. You can access and customize it via mocks.context:
import { createRenderer } from '@hubspot/ui-extensions/testing';
import { Text } from '@hubspot/ui-extensions';

function MyComponent({ context }) {
  return (
    <>
      <Text>User: {context.user.firstName} {context.user.lastName}</Text>
      <Text>Email: {context.user.email}</Text>
      <Text>Portal ID: {context.portal.id}</Text>
      {context.crm && <Text>Object ID: {context.crm.objectId}</Text>}
    </>
  );
}

const { render, mocks, findAll } = createRenderer('crm.record.tab');

// Access and verify the mock context values
expect(mocks.context.user.email).toEqual('fake_email@example.com');
expect(mocks.context.user.firstName).toEqual('fake_firstName');
expect(mocks.context.portal.id).toEqual(123);
expect(mocks.context.crm.objectId).toEqual(123);

render(<MyComponent context={mocks.context} />);

const textNodes = findAll(Text);
expect(textNodes[0].text).toEqual('User: fake_firstName fake_lastName');
expect(textNodes[1].text).toEqual('Email: fake_email@example.com');
You can customize the context before rendering:
const { findAll, render, mocks } = createRenderer('crm.record.tab');

// Customize context values for your test
mocks.context.user.firstName = 'Alice';
mocks.context.user.lastName = 'Johnson';
mocks.context.portal.id = 456;

render(<MyComponent context={mocks.context} />);
const textNodes = findAll(Text);
expect(textNodes[0].text).toEqual('User: Alice Johnson');
The shape of the context object depends on the extension point location:
  • CRM locations ('crm.record.tab', 'crm.record.sidebar', 'crm.preview', 'helpdesk.sidebar'): Include user, portal, crm (with objectId and objectTypeId), and variables
  • Settings location ('settings'): Include user and portal
  • Home location ('home'): Include user and portal

Mocking actions

The actions object contains extension point-specific functions that are automatically mocked as spies. You can use these spies to verify that your component calls the correct actions:
import { createRenderer } from '@hubspot/ui-extensions/testing';
import { Button } from '@hubspot/ui-extensions';

function MyComponent({ actions }) {
  return (
    <Button
      onClick={() => {
        actions.addAlert({
          type: 'success',
          message: 'Action completed!',
        });
      }}
    >
      Trigger Alert
    </Button>
  );
}

const { render, mocks, find } = createRenderer('crm.record.tab');

render(<MyComponent actions={mocks.actions} />);

// Trigger the button click
find(Button).trigger('onClick');

// Verify the action was called
expect(mocks.actions.addAlert.called).toBe(true);
expect(mocks.actions.addAlert.callCount).toBe(1);
expect(mocks.actions.addAlert.calls[0]).toEqual([
  {
    type: 'success',
    message: 'Action completed!',
  },
]);
Available actions vary by extension point location:
  • CRM locations: addAlert(), reloadPage(), fetchCrmObjectProperties(), openIframeModal(), refreshObjectProperties(), onCrmPropertiesUpdate(), copyTextToClipboard(), closeOverlay()
  • Settings location: addAlert(), copyTextToClipboard(), closeOverlay(), reloadPage(), openIframeModal()
  • Home location: addAlert(), copyTextToClipboard(), closeOverlay(), reloadPage(), openIframeModal()

Mocking serverless functions

The runServerlessFunction() mock allows you to test components that call serverless functions:
import { useState } from 'react';
import { createRenderer } from '@hubspot/ui-extensions/testing';
import { Button, Text } from '@hubspot/ui-extensions';

function MyComponent({ runServerlessFunction }) {
  const [data, setData] = useState(null);

  const handleClick = async () => {
    const result = await runServerlessFunction({
      name: 'myFunction',
      parameters: { param1: 'value1' },
    });
    if (result.status === 'SUCCESS') {
      setData(result.response);
    }
  };

  return (
    <>
      <Button onClick={handleClick}>Fetch Data</Button>
      {data && <Text>{data.message}</Text>}
    </>
  );
}

const { render, mocks, find, waitFor } = createRenderer('crm.record.tab');

// Mock the serverless function response
mocks.runServerlessFunction.nextResult(
  Promise.resolve({
    status: 'SUCCESS',
    response: { message: 'Hello from serverless!' },
  })
);

render(<MyComponent runServerlessFunction={mocks.runServerlessFunction} />);

// Trigger the function call
find(Button).trigger('onClick');

// Wait for the async update
await waitFor(() => {
  expect(find(Text).text).toEqual('Hello from serverless!');
});

// Verify the function was called correctly
expect(mocks.runServerlessFunction.callCount).toBe(1);
expect(mocks.runServerlessFunction.calls[0]).toEqual([
  {
    name: 'myFunction',
    parameters: { param1: 'value1' },
  },
]);
You can also use willCall() to provide custom logic:
mocks.runServerlessFunction.willCall(async ({ name, parameters }) => {
  if (name === 'myFunction') {
    return {
      status: 'SUCCESS',
      response: { result: parameters.param1.toUpperCase() },
    };
  }
  return {
    status: 'ERROR',
    error: 'Function not found',
  };
});

Advanced mock features

Since mocks are built on tinyspy, they expose additional properties and methods for advanced testing scenarios:

Spy properties

All mocks provide the following properties to inspect how the function was called:
  • called: boolean indicating if the function was called
  • callCount: number of times the function was called
  • calls: array of argument arrays for each call
  • results: array of results for each call (in format ['ok', value] or ['error', error])
  • returns: array of return values for each successful call
import { createRenderer } from '@hubspot/ui-extensions/testing';
import { useCrmProperties } from '@hubspot/ui-extensions/crm';
import { Text } from '@hubspot/ui-extensions';

function MyComponent() {
  const { properties } = useCrmProperties(['firstname', 'lastname']);
  return <Text>{properties.firstname}</Text>;
}

const { render, mocks } = createRenderer('crm.record.tab');
render(<MyComponent />);

// Inspect how the hook was called
expect(mocks.useCrmProperties.called).toBe(true);
expect(mocks.useCrmProperties.callCount).toBe(1);
expect(mocks.useCrmProperties.calls).toEqual([[['firstname', 'lastname']]]);
expect(mocks.useCrmProperties.returns[0]).toMatchObject({
  properties: { firstname: 'fake_firstname', lastname: 'fake_lastname' },
  error: null,
  isLoading: false,
});

Resetting mocks

Use the reset() method to clear all call history while keeping the mock implementation. This is useful when you want to reuse a mock across multiple test cases:
const { render, mocks } = createRenderer('crm.record.tab');

render(<MyComponent />);
expect(mocks.useCrmProperties.callCount).toBe(1);

// Clear call history
mocks.useCrmProperties.reset();
expect(mocks.useCrmProperties.callCount).toBe(0);
expect(mocks.useCrmProperties.called).toBe(false);
expect(mocks.useCrmProperties.calls).toEqual([]);

// Mock implementation still works for future calls
render(<MyComponent />);
expect(mocks.useCrmProperties.callCount).toBe(1);

Restoring original implementation

Use the restore() method to restore the mock to its default mocked implementation. This is useful when you’ve customized a mock with willCall() and want to return to the default behavior:
const { render, mocks, find } = createRenderer('crm.record.tab');

// Provide a custom implementation
mocks.useCrmProperties.willCall(() => ({
  properties: { firstname: 'Custom' },
  error: null,
  isLoading: false,
}));

render(<MyComponent />);
expect(find(Text).text).toEqual('First name: Custom');

// Restore to default mock behavior
mocks.useCrmProperties.restore();

// Now it uses the default fake data again
render(<MyComponent />);
expect(find(Text).text).toEqual('First name: fake_firstname');
For more advanced usage and features, see the tinyspy documentation.