Files
sim/.claude/rules/sim-testing.md
Waleed 4ccb57371b improvement(tests): speed up unit tests by eliminating vi.resetModules anti-pattern (#3357)
* improvement(tests): speed up unit tests by eliminating vi.resetModules anti-pattern

- convert 51 test files from vi.resetModules/vi.doMock/dynamic import to vi.hoisted/vi.mock/static import
- add global @sim/db mock to vitest.setup.ts
- switch 4 test files from jsdom to node environment
- remove all vi.importActual calls that loaded heavy modules (200+ block files)
- remove slow mockConsoleLogger/mockAuth/setupCommonApiMocks helpers
- reduce real setTimeout delays in engine tests
- mock heavy transitive deps in diff-engine test

test execution time: 34s -> 9s (3.9x faster)
environment time: 2.5s -> 0.6s (4x faster)

* docs(testing): update testing best practices with performance rules

- document vi.hoisted + vi.mock + static import as the standard pattern
- explicitly ban vi.resetModules, vi.doMock, vi.importActual, mockAuth, setupCommonApiMocks
- document global mocks from vitest.setup.ts
- add mock pattern reference for auth, hybrid auth, and database chains
- add performance rules section covering heavy deps, jsdom vs node, real timers

* fix(tests): fix 4 failing test files with missing mocks

- socket/middleware/permissions: add vi.mock for @/lib/auth to prevent transitive getBaseUrl() call
- workflow-handler: add vi.mock for @/executor/utils/http matching executor mock pattern
- evaluator-handler: add db.query.account mock structure before vi.spyOn
- router-handler: same db.query.account fix as evaluator

* fix(tests): replace banned Function type with explicit callback signature
2026-02-26 15:46:49 -08:00

6.0 KiB

paths
paths
apps/sim/**/*.test.ts
apps/sim/**/*.test.tsx

Testing Patterns

Use Vitest. Test files: feature.tsfeature.test.ts

Global Mocks (vitest.setup.ts)

These modules are mocked globally — do NOT re-mock them in test files unless you need to override behavior:

  • @sim/dbdatabaseMock
  • drizzle-ormdrizzleOrmMock
  • @sim/loggerloggerMock
  • @/stores/console/store, @/stores/terminal, @/stores/execution/store
  • @/blocks/registry
  • @trigger.dev/sdk

Structure

/**
 * @vitest-environment node
 */
import { createMockRequest } from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'

const { mockGetSession } = vi.hoisted(() => ({
  mockGetSession: vi.fn(),
}))

vi.mock('@/lib/auth', () => ({
  auth: { api: { getSession: vi.fn() } },
  getSession: mockGetSession,
}))

import { GET, POST } from '@/app/api/my-route/route'

describe('my route', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })
  })

  it('returns data', async () => {
    const req = createMockRequest('GET')
    const res = await GET(req)
    expect(res.status).toBe(200)
  })
})

Performance Rules (Critical)

NEVER use vi.resetModules() + vi.doMock() + await import()

This is the #1 cause of slow tests. It forces complete module re-evaluation per test.

// BAD — forces module re-evaluation every test (~50-100ms each)
beforeEach(() => {
  vi.resetModules()
  vi.doMock('@/lib/auth', () => ({ getSession: vi.fn() }))
})
it('test', async () => {
  const { GET } = await import('./route')  // slow dynamic import
})

// GOOD — module loaded once, mocks reconfigured per test (~1ms each)
const { mockGetSession } = vi.hoisted(() => ({
  mockGetSession: vi.fn(),
}))
vi.mock('@/lib/auth', () => ({ getSession: mockGetSession }))
import { GET } from '@/app/api/my-route/route'

beforeEach(() => { vi.clearAllMocks() })
it('test', () => {
  mockGetSession.mockResolvedValue({ user: { id: '1' } })
})

Only exception: Singleton modules that cache state at module scope (e.g., Redis clients, connection pools). These genuinely need vi.resetModules() + dynamic import to get a fresh instance per test.

NEVER use vi.importActual()

This defeats the purpose of mocking by loading the real module and all its dependencies.

// BAD — loads real module + all transitive deps
vi.mock('@/lib/workspaces/utils', async () => {
  const actual = await vi.importActual('@/lib/workspaces/utils')
  return { ...actual, myFn: vi.fn() }
})

// GOOD — mock everything, only implement what tests need
vi.mock('@/lib/workspaces/utils', () => ({
  myFn: vi.fn(),
  otherFn: vi.fn(),
}))

NEVER use mockAuth(), mockConsoleLogger(), or setupCommonApiMocks() from @sim/testing

These helpers internally use vi.doMock() which is slow. Use direct vi.hoisted() + vi.mock() instead.

Mock heavy transitive dependencies

If a module under test imports @/blocks (200+ files), @/tools/registry, or other heavy modules, mock them:

vi.mock('@/blocks', () => ({
  getBlock: () => null,
  getAllBlocks: () => ({}),
  getAllBlockTypes: () => [],
  registry: {},
}))

Use @vitest-environment node unless DOM is needed

Only use @vitest-environment jsdom if the test uses window, document, FormData, or other browser APIs. Node environment is significantly faster.

Avoid real timers in tests

// BAD
await new Promise(r => setTimeout(r, 500))

// GOOD — use minimal delays or fake timers
await new Promise(r => setTimeout(r, 1))
// or
vi.useFakeTimers()

Mock Pattern Reference

Auth mocking (API routes)

const { mockGetSession } = vi.hoisted(() => ({
  mockGetSession: vi.fn(),
}))

vi.mock('@/lib/auth', () => ({
  auth: { api: { getSession: vi.fn() } },
  getSession: mockGetSession,
}))

// In tests:
mockGetSession.mockResolvedValue({ user: { id: 'user-1', email: 'test@example.com' } })
mockGetSession.mockResolvedValue(null) // unauthenticated

Hybrid auth mocking

const { mockCheckSessionOrInternalAuth } = vi.hoisted(() => ({
  mockCheckSessionOrInternalAuth: vi.fn(),
}))

vi.mock('@/lib/auth/hybrid', () => ({
  checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
}))

// In tests:
mockCheckSessionOrInternalAuth.mockResolvedValue({
  success: true, userId: 'user-1', authType: 'session',
})

Database chain mocking

const { mockSelect, mockFrom, mockWhere } = vi.hoisted(() => ({
  mockSelect: vi.fn(),
  mockFrom: vi.fn(),
  mockWhere: vi.fn(),
}))

vi.mock('@sim/db', () => ({
  db: { select: mockSelect },
}))

beforeEach(() => {
  mockSelect.mockReturnValue({ from: mockFrom })
  mockFrom.mockReturnValue({ where: mockWhere })
  mockWhere.mockResolvedValue([{ id: '1', name: 'test' }])
})

@sim/testing Package

Always prefer over local test data.

Category Utilities
Mocks loggerMock, databaseMock, drizzleOrmMock, setupGlobalFetchMock()
Factories createSession(), createWorkflowRecord(), createBlock(), createExecutionContext()
Builders WorkflowBuilder, ExecutionContextBuilder
Assertions expectWorkflowAccessGranted(), expectBlockExecuted()
Requests createMockRequest(), createEnvMock()

Rules Summary

  1. @vitest-environment node unless DOM is required
  2. vi.hoisted() + vi.mock() + static imports — never vi.resetModules() + vi.doMock() + dynamic imports
  3. vi.mock() calls before importing mocked modules
  4. @sim/testing utilities over local mocks
  5. beforeEach(() => vi.clearAllMocks()) to reset state — no redundant afterEach
  6. No vi.importActual() — mock everything explicitly
  7. No mockAuth(), mockConsoleLogger(), setupCommonApiMocks() — use direct mocks
  8. Mock heavy deps (@/blocks, @/tools/registry, @/triggers) in tests that don't need them
  9. Use absolute imports in test files
  10. Avoid real timers — use 1ms delays or vi.useFakeTimers()