Skip to content

@memberjunction/unit-testing

Utilities and mocks for writing unit tests in the MemberJunction monorepo using Vitest.

Terminal window
npm install --save-dev @memberjunction/unit-testing

This package is typically already included as a dev dependency in MemberJunction projects.

This package provides helper functions and mock utilities to simplify unit testing of MemberJunction components. It handles common testing challenges like:

  • Singleton reset - Clean state between tests
  • Entity mocking - Mock BaseEntity behavior without database
  • RunView mocking - Mock data loading operations
  • Custom matchers - Additional Vitest assertions

MemberJunction uses singletons for engines and global state. Reset them between tests to ensure isolation.

Clears ALL MJ singleton instances from the global store.

import { describe, it, beforeEach } from 'vitest';
import { resetMJSingletons } from '@memberjunction/unit-testing';
describe('MyEngine', () => {
beforeEach(() => {
resetMJSingletons(); // Clean slate for each test
});
it('should create fresh engine instance', () => {
const engine = MyEngine.Instance; // Gets new instance
// ... test
});
});

Resets only the ClassFactory registrations. Lighter weight than resetMJSingletons.

import { resetClassFactory } from '@memberjunction/unit-testing';
beforeEach(() => {
resetClassFactory(); // Only reset class registrations
});

Clears the global object cache used by MJ for caching data.

import { resetObjectCache } from '@memberjunction/unit-testing';
beforeEach(() => {
resetObjectCache(); // Clear cached objects
});

Creates a Proxy-based mock that behaves like a BaseEntity with getter/setter properties.

Why needed: BaseEntity uses getters/setters, so the spread operator (...entity) doesn’t work. This mock provides the same interface without requiring a real database.

import { createMockEntity } from '@memberjunction/unit-testing';
// Create a mock user entity
const mockUser = createMockEntity({
ID: 'user-123',
Name: 'Test User',
Email: 'test@example.com',
Status: 'Active'
});
// Use like a real entity
console.log(mockUser.Name); // 'Test User'
mockUser.Status = 'Inactive'; // Setter works
console.log(mockUser.Get('Email')); // 'test@example.com'
console.log(mockUser.GetAll()); // { ID: '...', Name: '...', ... }
await mockUser.Save(); // Mock save (always succeeds)
console.log(mockUser.Dirty); // false after save

Options:

interface MockEntityOptions {
isSaved?: boolean; // Default: true - Entity appears saved to DB
isDirty?: boolean; // Default: false - Entity appears clean
}
// Create unsaved entity
const newEntity = createMockEntity(
{ ID: '', Name: 'New User' },
{ isSaved: false, isDirty: true }
);

Mock Entity Methods:

  • Get(fieldName) - Get field value (case-insensitive)
  • Set(fieldName, value) - Set field value (marks dirty)
  • GetAll() - Returns all fields as plain object
  • Save() - Mock save operation (always succeeds, clears dirty flag)
  • Delete() - Mock delete operation (always succeeds)
  • Dirty - Boolean indicating if entity has unsaved changes
  • IsSaved - Boolean indicating if entity exists in DB
  • PrimaryKey - Mock primary key object

Mocks a single RunView operation to return specific results.

import { mockRunView } from '@memberjunction/unit-testing';
import { vi } from 'vitest';
// Mock RunView for 'Users' entity
const mockUsers = [
createMockEntity({ ID: '1', Name: 'Alice' }),
createMockEntity({ ID: '2', Name: 'Bob' })
];
const runViewSpy = mockRunView('Users', mockUsers);
// Now when code calls RunView:
const rv = new RunView();
const result = await rv.RunView({ EntityName: 'Users' });
// result.Results === mockUsers
// Verify it was called
expect(runViewSpy).toHaveBeenCalledWith(
expect.objectContaining({ EntityName: 'Users' })
);

Mocks multiple RunView operations at once.

import { mockRunViews } from '@memberjunction/unit-testing';
mockRunViews({
'Users': mockUsers,
'Actions': mockActions,
'AI Models': mockModels
});
// All three entities will return mock data

Clears all RunView mocks.

import { resetRunViewMocks } from '@memberjunction/unit-testing';
afterEach(() => {
resetRunViewMocks(); // Clean up mocks
});

Installs additional Vitest matchers for MemberJunction-specific assertions.

import { installCustomMatchers } from '@memberjunction/unit-testing';
import { beforeAll } from 'vitest';
beforeAll(() => {
installCustomMatchers();
});
// Now use custom matchers in your tests
// (See vitest.d.ts for available custom matchers)
import { describe, it, expect, beforeEach } from 'vitest';
import {
resetMJSingletons,
createMockEntity,
mockRunView
} from '@memberjunction/unit-testing';
describe('MyService', () => {
beforeEach(() => {
// Reset singletons for clean state
resetMJSingletons();
});
it('should process users correctly', async () => {
// Setup mock data
const mockUsers = [
createMockEntity({ ID: '1', Name: 'Alice', Status: 'Active' }),
createMockEntity({ ID: '2', Name: 'Bob', Status: 'Inactive' })
];
// Mock RunView to return mock data
mockRunView('Users', mockUsers);
// Test your service
const service = new MyService();
const result = await service.getActiveUsers();
// Assertions
expect(result).toHaveLength(1);
expect(result[0].Name).toBe('Alice');
});
});

When testing code that creates entities via Metadata.GetEntityObject():

import { vi } from 'vitest';
import { createMockEntity } from '@memberjunction/unit-testing';
// Mock the Metadata class
vi.mock('@memberjunction/core', () => ({
Metadata: vi.fn(function() {
return {
GetEntityObject: vi.fn(async (entityName) => {
return createMockEntity({ ID: '', Name: '' }, { isSaved: false });
})
};
})
}));
import { resetMJSingletons } from '@memberjunction/unit-testing';
describe('ActionEngine', () => {
beforeEach(() => {
resetMJSingletons(); // Ensures fresh instance
});
it('should load actions', async () => {
const engine = ActionEngine.Instance;
await engine.Load();
expect(engine.Actions.length).toBeGreaterThan(0);
});
it('should get separate instance after reset', async () => {
const engine1 = ActionEngine.Instance;
resetMJSingletons();
const engine2 = ActionEngine.Instance;
expect(engine1).not.toBe(engine2); // Different instances
});
});

This package includes TypeScript definitions for all utilities. Import types as needed:

import type { MockEntityOptions, MockEntityMethods } from '@memberjunction/unit-testing';
  • ✅ Always reset singletons in beforeEach() for test isolation
  • ✅ Use createMockEntity() instead of plain objects for BaseEntity mocks
  • ✅ Use mockRunView() to avoid database dependencies in unit tests
  • ✅ Keep mocks simple and focused on the test case
  • ❌ Share mock data between tests (creates hidden dependencies)
  • ❌ Forget to reset singletons (causes test interference)
  • ❌ Over-mock (test real logic where possible)
  • ❌ Use real database connections in unit tests (use integration tests instead)
  • Testing Strategy: See /unit-testing/README.md for comprehensive testing guidelines
  • Analytics: See /unit-testing/README.md for test reporting and analytics
  • Integration Tests: See /packages/TestingFramework/ for full-stack testing

Look at existing tests for usage examples:

  • /packages/MJCore/src/__tests__/ - Core functionality tests
  • /packages/Actions/Engine/src/__tests__/ - Action engine tests
  • /packages/AI/*/src/__tests__/ - AI provider tests

When adding new test utilities:

  1. Add the utility function to appropriate file (singleton-reset.ts, mock-entity.ts, etc.)
  2. Export from index.ts
  3. Update this README with usage examples
  4. Add TypeScript definitions to vitest.d.ts if adding custom matchers

See repository root LICENSE file.