Motia Icon
Development Guide

Testing

Test your Motia Backend System - APIs, Events

You built an API. You added some event handlers. Everything seems to work. But does it?

Without tests, you're guessing. With tests, you know.

Motia has @motiadev/test built in. It helps you:

  • Test API triggers (hit endpoints, check responses)
  • Test Event triggers (verify events get emitted)
  • Mock contexts for unit tests

Install

npm install @motiadev/test --save-dev
pnpm add @motiadev/test --save-dev

Test API Triggers

Here's an API Step that creates a todo and emits an event:

steps/create-todo.step.ts
export const config: ApiRouteConfig = {
  type: 'api',
  name: 'CreateTodo',
  path: '/todo',
  method: 'POST',
  emits: ['todo.created'],
  bodySchema: z.object({ description: z.string() })
}
 
export const handler: Handlers['CreateTodo'] = async (req, { emit }) => {
  const todo = { id: '123', description: req.body.description }
  
  await emit({ topic: 'todo.created', data: todo })
  
  return { status: 200, body: todo }
}

Now let's test it:

steps/create-todo.step.test.ts
import { createMotiaTester } from '@motiadev/test'
import { describe, it, expect, afterAll } from 'vitest'
 
describe('CreateTodo', () => {
  const tester = createMotiaTester()
 
  afterAll(async () => {
    await tester.close()
  })
 
  it('should create a todo and return 200', async () => {
    const response = await tester.post('/todo', {
      body: { description: 'Buy milk' }
    })
 
    expect(response.status).toBe(200)
    expect(response.body).toMatchObject({
      id: expect.any(String),
      description: 'Buy milk'
    })
  })
 
  it('should emit todo.created event', async () => {
    const watcher = await tester.watch('todo.created')
 
    await tester.post('/todo', {
      body: { description: 'Buy bread' }
    })
 
    await tester.waitEvents()
 
    const events = watcher.getCapturedEvents()
    expect(events).toHaveLength(1)
    expect(events[0].data).toMatchObject({
      description: 'Buy bread'
    })
  })
})

What's happening here:

  • createMotiaTester() → Spins up a test version of your app
  • tester.post() → Hits your API like a real client would
  • tester.watch() → Captures events that get emitted
  • tester.waitEvents() → Waits for all async stuff to finish
  • Then check if everything worked

Test Event Triggers

Event Steps listen for events and do stuff in the background. Here's how to test them:

steps/process-todo.step.ts
export const config: EventConfig = {
  type: 'event',
  name: 'ProcessTodo',
  subscribes: ['todo.created'],
  emits: ['todo.processed'],
  input: z.object({ id: z.string(), description: z.string() })
}
 
export const handler: Handlers['ProcessTodo'] = async (input, { emit, logger }) => {
  logger.info('Processing todo', { id: input.id })
  
  // Do some processing
  const processed = { ...input, processed: true }
  
  await emit({ topic: 'todo.processed', data: processed })
}

The Test:

steps/process-todo.step.test.ts
import { createMotiaTester } from '@motiadev/test'
import { describe, it, expect, afterAll } from 'vitest'
 
describe('ProcessTodo', () => {
  const tester = createMotiaTester()
 
  afterAll(async () => {
    await tester.close()
  })
 
  it('should process todo when todo.created is emitted', async () => {
    const watcher = await tester.watch('todo.processed')
 
    // Manually emit the event that triggers the step
    await tester.emit({
      topic: 'todo.created',
      data: { id: '123', description: 'Test todo' },
      traceId: 'test-trace'
    })
 
    await tester.waitEvents()
 
    const events = watcher.getCapturedEvents()
    expect(events).toHaveLength(1)
    expect(events[0].data).toMatchObject({
      id: '123',
      description: 'Test todo',
      processed: true
    })
  })
})

👉 Use tester.emit() to manually fire events and test Event triggers without hitting APIs.


Unit Test Handlers

Don't want to spin up the whole app? Test handler functions directly:

steps/calculate-total.step.test.ts
import { createMockContext } from '@motiadev/test'
import { handler } from './calculate-total.step'
import { describe, it, expect } from 'vitest'
 
describe('CalculateTotal Handler', () => {
  it('should calculate total correctly', async () => {
    const mockContext = createMockContext()
    
    const input = { items: [{ price: 10 }, { price: 20 }] }
    
    await handler(input, mockContext)
    
    expect(mockContext.emit).toHaveBeenCalledWith({
      topic: 'total.calculated',
      data: { total: 30 }
    })
  })
 
  it('should log calculation', async () => {
    const mockContext = createMockContext()
    
    await handler({ items: [] }, mockContext)
    
    expect(mockContext.logger.info).toHaveBeenCalledWith(
      expect.stringContaining('Calculating total')
    )
  })
})

Run Your Tests

All tests:

npm test
pnpm test

Watch mode (re-runs when you save files):

npm test -- --watch
pnpm test --watch

Single test file:

npm test -- steps/create-todo.step.test.ts

Tester API

createMotiaTester()

Starts a test version of your app.

const tester = createMotiaTester()

What you can do with it:

MethodWhat it does
post(path, options)Hit a POST endpoint
get(path, options)Hit a GET endpoint
emit(event)Fire an event manually
watch(topic)Catch events on a topic
waitEvents()Wait for events to finish
sleep(ms)Pause for X milliseconds
close()Shut down the tester

createMockContext()

Mock a context for testing handlers directly.

const mockContext = createMockContext({
  logger: customLogger,  // optional
  emit: customEmit,      // optional
  traceId: 'custom-id'   // optional
})

You get:

  • logger - Mock logger (Jest spy)
  • emit - Mock emit (Jest spy)
  • traceId - Request trace ID
  • state - Mock state manager

Tips

  • Start simple - Test basic stuff first, then edge cases
  • Test errors - Make sure your error handling actually works
  • Watch events - Don't assume events fired, check them
  • Always wait - Call waitEvents() or events might not finish
  • Clean up - Always close() the tester when done
  • Keep it isolated - Each test should work on its own
  • Name tests well - Say what you're checking, not how

Need help? See our Community Resources for questions, examples, and discussions.

On this page