This guide shows integration developers how to write tests for Polarity integrations using Vitest and the test helpers from polarity-integration-utils/testing.
Install testing dependencies:
npm install --save-dev vitest
Add a Vitest config (vitest.config.ts):
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
include: ['test/**/*.test.ts']
}
});
Add a test script to package.json:
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest"
}
}
Run tests:
npm test
Integrations use PolarityRequest to make HTTP requests. In tests, you mock the PolarityRequest module so that calls to request.run() return controlled responses without hitting real APIs.
Use vi.hoisted() to create the mock run function, then use vi.mock() to replace the PolarityRequest class with a mock that uses it:
import { vi, describe, it, expect, beforeEach, beforeAll } from 'vitest';
import type {
Entity,
DoLookupUserOptions,
IntegrationContext
} from '@polarityio/integration-types';
import type { HttpRequestResponse } from 'polarity-integration-utils';
import { createMockIntegrationContext } from 'polarity-integration-utils/testing';
// Hoist mock functions so they're available before vi.mock() runs
const { mockRun } = vi.hoisted(() => ({
mockRun: vi.fn()
}));
// Mocking the sub-path works even if your integration imports from the root,
// because the root module re-exports from this sub-path internally.
vi.mock('polarity-integration-utils/requests', () => {
return {
PolarityRequest: vi.fn().mockImplementation(() => ({
run: mockRun,
runInParallel: vi.fn(),
userOptions: null,
limiter: null,
hooks: { beforeRequest: [], afterResponse: [], onApiError: [], onNetworkError: [] }
}))
};
});
// Import your integration AFTER vi.mock() so it receives the mocked module
import { doLookup, startup } from '../src/integration';
// Shorthand for creating a mock context with Vitest spies
const createMockContext = () => createMockIntegrationContext(vi.fn);
Create reusable helpers that configure mockRun to resolve or reject:
function mockRunSuccess(body: unknown, statusCode = 200): void {
mockRun.mockResolvedValue({
statusCode,
body
} as HttpRequestResponse);
}
function mockRunError(error: Error): void {
mockRun.mockRejectedValue(error);
}
describe('doLookup', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should return results for a successful API response', async () => {
mockRunSuccess({
ip: '8.8.8.8',
hostname: 'dns.google',
org: 'Google LLC',
country: 'US'
});
const entities: Entity[] = [createEntity('IPv4', '8.8.8.8')];
const options: DoLookupUserOptions = { apiKey: 'test-key' };
const results = await doLookup(entities, options, createMockContext());
expect(results).toHaveLength(1);
expect(results[0].data).not.toBeNull();
expect(mockRun).toHaveBeenCalledTimes(1);
});
it('should handle empty results', async () => {
mockRunSuccess({ ip: '8.8.8.8', bogon: true });
const entities: Entity[] = [createEntity('IPv4', '8.8.8.8')];
const options: DoLookupUserOptions = { apiKey: 'test-key' };
const results = await doLookup(entities, options, createMockContext());
expect(results[0].data).toBeNull();
});
it('should throw when the API returns an error', async () => {
const { ApiRequestError } = await import('polarity-integration-utils');
mockRunError(new ApiRequestError('Forbidden'));
const entities: Entity[] = [createEntity('IPv4', '8.8.8.8')];
const options: DoLookupUserOptions = { apiKey: 'invalid-key' };
await expect(
doLookup(entities, options, createMockContext())
).rejects.toThrow('Forbidden');
});
it('should throw on network errors', async () => {
const { NetworkError } = await import('polarity-integration-utils');
mockRunError(new NetworkError('ECONNREFUSED'));
const entities: Entity[] = [createEntity('domain', 'unreachable.example.com')];
const options: DoLookupUserOptions = { apiKey: 'test-key' };
await expect(
doLookup(entities, options, createMockContext())
).rejects.toThrow('ECONNREFUSED');
});
});
You can inspect how your integration called request.run() to verify it builds the correct request:
it('should pass the correct URL and headers', async () => {
mockRunSuccess({ result: 'ok' });
const entities: Entity[] = [createEntity('IPv4', '8.8.8.8')];
const options: DoLookupUserOptions = { apiKey: 'my-key' };
await doLookup(entities, options, createMockContext());
expect(mockRun).toHaveBeenCalledWith(
expect.objectContaining({
url: expect.stringContaining('8.8.8.8'),
headers: expect.objectContaining({
Authorization: 'Bearer my-key'
})
})
);
});
createEntity(type, value)Creates a fully formed Entity object for testing.
import { createEntity } from 'polarity-integration-utils/testing';
const ipEntity = createEntity('IPv4', '8.8.8.8');
const domainEntity = createEntity('domain', 'example.com');
const hashEntity = createEntity('MD5', 'd41d8cd98f00b204e9800998ecf8427e');
type: An EntityType string (e.g., 'IPv4', 'IPv6', 'domain', 'MD5', 'SHA256')value: The entity value stringcreateMockIntegrationContext(createMockFn?)Creates a mock IntegrationContext with stubbed logger and cache methods. Pass your testing framework's mock function factory to enable spy capabilities (e.g., toHaveBeenCalledWith()).
import { createMockIntegrationContext } from 'polarity-integration-utils/testing';
// With Vitest — enables assertions on mock calls
const context = createMockIntegrationContext(vi.fn);
// With Jest
const context = createMockIntegrationContext(jest.fn);
// Without a framework — uses plain no-op stubs
const context = createMockIntegrationContext();
The returned context includes:
context.logger — stubbed logger (trace, debug, info, warn, error, fatal, child)context.cache.global / integration / user — stubbed get, set, deletecontext.startPolling / stopPolling — stubbed functionsWhen a mock factory is provided, logger.child() is automatically wired to return the logger for method chaining.
validateOptionsimport type { ValidateOptionsUserOptions } from '@polarityio/integration-types';
describe('validateOptions', () => {
it('should pass with valid options', () => {
const options: ValidateOptionsUserOptions = {
apiKey: {
key: 'apiKey',
value: 'valid-key',
integration_id: 'test-integration',
user_can_edit: true,
admin_only: false
}
};
const errors = validateOptions(options, createMockContext());
expect(errors).toHaveLength(0);
});
it('should return errors for missing required options', () => {
const options: ValidateOptionsUserOptions = {
apiKey: {
key: 'apiKey',
value: '',
integration_id: 'test-integration',
user_can_edit: true,
admin_only: false
}
};
const errors = validateOptions(options, createMockContext());
expect(errors.length).toBeGreaterThan(0);
expect(errors[0].key).toBe('apiKey');
});
});
The mock context created by createMockIntegrationContext(vi.fn) includes stubbed cache methods. You can assert that your integration reads from or writes to the cache:
describe('cache operations', () => {
it('should cache lookup results', async () => {
mockRunSuccess({ ip: '8.8.8.8', org: 'Google LLC' });
const context = createMockIntegrationContext(vi.fn);
const entities: Entity[] = [createEntity('IPv4', '8.8.8.8')];
const options: DoLookupUserOptions = { apiKey: 'test-key' };
await doLookup(entities, options, context);
expect(context.cache.integration.set).toHaveBeenCalledWith(
'8.8.8.8',
expect.objectContaining({ org: 'Google LLC' })
);
});
it('should return cached data when available', async () => {
const context = createMockIntegrationContext(vi.fn);
const cached = { ip: '8.8.8.8', org: 'Google LLC' };
(context.cache.integration.get as ReturnType<typeof vi.fn>)
.mockResolvedValue(cached);
const entities: Entity[] = [createEntity('IPv4', '8.8.8.8')];
const options: DoLookupUserOptions = { apiKey: 'test-key' };
await doLookup(entities, options, context);
// Verify cache was checked and no HTTP request was made
expect(context.cache.integration.get).toHaveBeenCalledWith('8.8.8.8');
expect(mockRun).not.toHaveBeenCalled();
});
});
PolarityRequest level — Mock request.run() rather than the underlying HTTP library. This keeps tests decoupled from internal implementation details.vi.clearAllMocks() in beforeEach to prevent state leaking between tests.vi.hoisted() — When using vi.mock(), any mock functions referenced inside the factory must be hoisted so they exist before the mock is applied.vi.mock() calls so it receives the mocked dependencies.expect(mockRun).toHaveBeenCalledWith(expect.objectContaining(...)) to assert your integration builds the correct request URL, headers, and body.