[iris]
Guides

Testing

Instant test environment reset with fork

The Problem

Traditional test environments suffer from state pollution:

  • Tests affect each other through shared database state
  • File system changes persist between runs
  • Flaky tests due to execution ordering

The Iris Solution

Keep one seeded base sandbox alive. Each test forks from it — fully isolated, zero setup overhead per test.

import { Sandbox } from '@iris/sdk'
import { describe, it, beforeAll, afterAll, afterEach } from 'vitest'

let base: Sandbox

beforeAll(async () => {
  base = await Sandbox.create()

  // Install deps, seed database, etc.
  await base.exec.run('npm install')
  await base.exec.run('npm run db:seed')
})

afterAll(async () => {
  await base.delete()
})

describe('User API', () => {
  let env: Sandbox

  beforeEach(async () => {
    env = await base.fork()
  })

  afterEach(async () => {
    await env.delete()
  })

  it('creates a user', async () => {
    const result = await env.exec.run('npm test -- user.create.test.ts')
    expect(result.exit_code).toBe(0)
  })

  it('deletes a user', async () => {
    const result = await env.exec.run('npm test -- user.delete.test.ts')
    expect(result.exit_code).toBe(0)
  })
})
import pytest
from iris import IrisClient

client = IrisClient()

@pytest.fixture(scope="session")
def base():
    sandbox = client.sandboxes.create()
    sandbox.exec.run(["bash", "-c", "npm install && npm run db:seed"])
    yield sandbox
    sandbox.delete()

@pytest.fixture
def env(base):
    fork = base.fork()
    yield fork
    fork.delete()

def test_creates_user(env):
    result = env.exec.run(["npm", "test", "--", "user.create.test.ts"])
    assert result.exit_code == 0

def test_deletes_user(env):
    result = env.exec.run(["npm", "test", "--", "user.delete.test.ts"])
    assert result.exit_code == 0

Parallel Test Execution

Fork from the same base in parallel — no interference:

const tests = [
  'auth.test.ts',
  'users.test.ts',
  'payments.test.ts',
  'notifications.test.ts',
]

const results = await Promise.all(
  tests.map(async (test) => {
    const env = await base.fork()
    const result = await env.exec.run(`npm test -- ${test}`)
    await env.delete()
    return { test, passed: result.exit_code === 0, output: result.stdout }
  }),
)

const failed = results.filter((r) => !r.passed)
if (failed.length > 0) {
  console.error('Failed:', failed.map((r) => r.test))
}
import asyncio
from iris import AsyncIrisClient

async def main():
    client = AsyncIrisClient()
    # assume base sandbox is already set up
    tests = ["auth.test.ts", "users.test.ts", "payments.test.ts", "notifications.test.ts"]

    async def run_test(test):
        env = await base.fork()
        result = await env.exec.run(["bash", "-c", f"npm test -- {test}"])
        await env.delete()
        return {"test": test, "passed": result.exit_code == 0, "output": result.stdout}

    results = await asyncio.gather(*[run_test(t) for t in tests])
    failed = [r for r in results if not r["passed"]]
    if failed:
        print("Failed:", [r["test"] for r in failed])

asyncio.run(main())

Integration Testing

Test against real services with a guaranteed-clean environment per suite:

async function integrationTest() {
  const base = await Sandbox.create()

  // Start services once
  await base.exec.run('docker-compose up -d')
  await base.exec.run('python3 wait_for_services.py')

  // Each suite gets a fresh fork — database is in the same state for all
  await Promise.all(
    testSuites.map(async (suite) => {
      const env = await base.fork()
      try {
        const result = await env.exec.run(`pytest ${suite}`)
        if (!result.ok) console.error(suite, result.stderr)
      } finally {
        await env.delete()
      }
    }),
  )

  await base.delete()
}
import asyncio
from iris import AsyncIrisClient

async def integration_test():
    client = AsyncIrisClient()
    base = await client.sandboxes.create()

    # Start services once
    await base.exec.run(["bash", "-c", "docker-compose up -d"])
    await base.exec.run(["python3", "wait_for_services.py"])

    # Each suite gets a fresh fork — database is in the same state for all
    async def run_suite(suite):
        env = await base.fork()
        try:
            result = await env.exec.run(["pytest", suite])
            if not result.ok:
                print(suite, result.stderr)
        finally:
            await env.delete()

    await asyncio.gather(*[run_suite(s) for s in test_suites])
    await base.delete()

asyncio.run(integration_test())

CI/CD Integration

GitHub Actions

name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install SDK
        run: npm install @iris/sdk

      - name: Run Tests
        env:
          IRIS_API_KEY: ${{ secrets.IRIS_API_KEY }}
        run: npm test

Performance Comparison

ApproachSetup TimePer-Test Overhead
Docker30-60s5-10s
VM Snapshot10-30s2-5s
Iris ForkOnce<1ms

Iris forks are 1000x faster than traditional VM snapshots because only the pages a test actually writes are copied — everything else is shared read-only with the base.

On this page