# Runtime behavior

> Learn about Bun test's runtime integration, environment variables, timeouts, and error handling

`bun test` is deeply integrated with Bun's runtime. This integration is part of what makes `bun test` fast.

## Environment Variables

### NODE_ENV

`bun test` sets `$NODE_ENV` to `"test"` unless it's already set in the environment or in `.env` files. Most test runners do the same.

```ts title="test.ts" icon="/icons/typescript.svg"
import { test, expect } from "bun:test";

test("NODE_ENV is set to test", () => {
  expect(process.env.NODE_ENV).toBe("test");
});
```

You can override this by setting `NODE_ENV` explicitly:

```bash terminal icon="terminal"
NODE_ENV=development bun test
```

### TZ (Timezone)

`bun test` uses UTC (`Etc/UTC`) as the time zone unless the `TZ` environment variable overrides it. This keeps date and time behavior consistent across machines.

```ts title="test.ts" icon="/icons/typescript.svg"
import { test, expect } from "bun:test";

test("timezone is UTC by default", () => {
  const date = new Date();
  expect(date.getTimezoneOffset()).toBe(0);
});
```

To test with a specific time zone:

```bash terminal icon="terminal"
TZ=America/New_York bun test
```

## Test Timeouts

Each test has a default timeout of 5000ms (5 seconds). Tests that exceed it fail.

### Global Timeout

Change the timeout globally with the `--timeout` flag:

```bash terminal icon="terminal"
bun test --timeout 10000  # 10 seconds
```

### Per-Test Timeout

Set a per-test timeout as the third argument to the test function:

```ts title="test.ts" icon="/icons/typescript.svg"
import { test, expect } from "bun:test";

test("fast test", () => {
  expect(1 + 1).toBe(2);
}, 1000); // 1 second timeout

test("slow test", async () => {
  await new Promise(resolve => setTimeout(resolve, 8000));
}, 10000); // 10 second timeout
```

### Infinite Timeout

Use `0` or `Infinity` to disable the timeout:

```ts title="test.ts" icon="/icons/typescript.svg"
test("test without timeout", async () => {
  // This test can run indefinitely
  await someVeryLongOperation();
}, 0);
```

## Error Handling

### Unhandled Errors

`bun test` tracks unhandled promise rejections and errors that occur between tests. If any occur, `bun test` exits with a non-zero code even when no test failed. In both examples below the error happens while the file is being loaded, so the file's tests are not run at all.

This helps catch errors in asynchronous code that might otherwise go unnoticed:

```ts title="test.ts" icon="/icons/typescript.svg"
import { test, expect } from "bun:test";

test("test 1", () => {
  expect(true).toBe(true);
});

// This error happens outside any test
queueMicrotask(() => {
  throw new Error("Unhandled error");
});

test("test 2", () => {
  expect(true).toBe(true);
});

// bun test reports this as "Unhandled error between tests", does not run
// this file's tests (0 pass, 1 error), and exits with code 1
```

### Promise Rejections

The test runner also catches unhandled promise rejections:

```ts title="test.ts" icon="/icons/typescript.svg"
import { test, expect } from "bun:test";

test("test 1", () => {
  expect(1).toBe(1);
});

// bun test reports this as "Unhandled error between tests", does not run
// this file's tests, and exits with code 1
Promise.reject(new Error("Unhandled rejection"));
```

### Custom Error Handling

You can set up custom error handlers in your test setup:

```ts title="test-setup.ts" icon="/icons/typescript.svg"
process.on("uncaughtException", error => {
  console.error("Uncaught Exception:", error);
  process.exit(1);
});

process.on("unhandledRejection", (reason, promise) => {
  console.error("Unhandled Rejection at:", promise, "reason:", reason);
  process.exit(1);
});
```

## CLI Flags Integration

Several Bun CLI flags also work with `bun test`:

### Memory Usage

```bash terminal icon="terminal"
# Reduces memory usage for the test runner VM
bun test --smol
```

### Debugging

```bash terminal icon="terminal"
# Attaches the debugger to the test runner process
bun test --inspect
bun test --inspect-brk
```

### Module Loading

```bash terminal icon="terminal"
# Runs scripts before test files (useful for global setup/mocks)
bun test --preload ./setup.ts

# Sets compile-time constants
bun test --define "process.env.API_URL='http://localhost:3000'"

# Maps file extensions to built-in loaders
bun test --loader .svg:text

# Uses a different tsconfig
bun test --tsconfig-override ./test-tsconfig.json

# Sets package.json conditions for module resolution
bun test --conditions development

# Loads environment variables for tests
bun test --env-file .env.test
```

### Installation-related Flags

```bash
# Affect any network requests or auto-installs during test execution
bun test --prefer-offline
bun test --frozen-lockfile
```

## Watch and Hot Reloading

### Watch Mode

With the `--watch` flag, the test runner watches for file changes and re-runs tests.

```bash terminal icon="terminal"
bun test --watch
```

### Hot Reloading

The `--hot` flag is similar, but more aggressive about preserving state between runs:

```bash terminal icon="terminal"
bun test --hot
```

For most tests, use `--watch`: it gives better isolation between runs.

## Global Variables

The following globals are available in test files without importing:

```ts title="test.ts" icon="/icons/typescript.svg"
// All of these are available globally
test("global test function", () => {
  expect(true).toBe(true);
});

describe("global describe", () => {
  beforeAll(() => {
    // global beforeAll
  });

  it("global it function", () => {
    // it is an alias for test
  });
});

// Jest compatibility
jest.fn();

// Vitest compatibility
vi.fn();
```

You can also import them explicitly:

```ts title="test.ts" icon="/icons/typescript.svg"
import { test, it, describe, expect, beforeAll, beforeEach, afterAll, afterEach, jest, vi } from "bun:test";
```

## Process Integration

### Exit Codes

`bun test` uses standard exit codes:

- `0`: All tests passed, no unhandled errors
- `1`: Test failures or unhandled errors occurred

### Signal Handling

The test runner handles common signals:

```bash terminal icon="terminal"
# Gracefully stops test execution
kill -SIGTERM <test-process-pid>

# Immediately stops test execution
kill -SIGKILL <test-process-pid>
```

### Environment Detection

Bun automatically detects certain environments and adjusts behavior:

```ts title="test.ts" icon="/icons/typescript.svg"
// GitHub Actions detection
if (process.env.GITHUB_ACTIONS) {
  // Bun automatically emits GitHub Actions annotations
}

// CI detection
if (process.env.CI) {
  // Certain behaviors may be adjusted for CI environments
}
```

## Performance Considerations

### Single Process

The test runner runs all tests in a single process by default. This provides:

- **Faster startup** - No need to spawn multiple processes
- **Shared memory** - Efficient resource usage
- **Simpler debugging** - All tests in one process

However, this means:

- Tests share global state (use lifecycle hooks to clean up)
- One test crash can affect others
- No true parallelization of individual tests

### Memory Management

```bash terminal icon="terminal"
# Monitor memory usage
bun test --smol  # Reduces memory footprint

# For large test suites, consider splitting files
bun test src/unit/
bun test src/integration/
```

### Test Isolation

Since tests run in the same process, ensure proper cleanup:

```ts title="test.ts" icon="/icons/typescript.svg"
import { afterEach, jest } from "bun:test";

afterEach(() => {
  // Clean up global state
  global.myGlobalVar = undefined;
  delete process.env.TEST_VAR;

  // Restore mocked functions if needed
  jest.restoreAllMocks();
});
```
