{"content":"# Unit Tests with Bun\n\nConfigure unit testing with Bun's built-in test runner. Fast, Jest-compatible syntax, co-located test files, and mocking support.\n\n## Install Bun Types\n\nAdd Bun types for full TypeScript support in test files:\n\n```bash\nbun add -D @types/bun\n```\n\n---\n\n## Folder Structure\n\nUnit tests live alongside source code in `src/`:\n\n```\nsrc/\n├── lib/\n│   ├── common/\n│   │   ├── assert.ts\n│   │   └── assert.test.ts      # Unit test for assert\n│   ├── config/\n│   │   ├── schema.ts\n│   │   └── schema.test.ts      # Unit test for config schema\n│   └── ...\n```\n\nTests use the `.test.ts` suffix and are co-located with the code they test.\n\n---\n\n## When to Write Unit Tests\n\nUse unit tests for:\n\n- Pure functions with complex logic (validation, parsing, transformation)\n- Utility functions without external dependencies\n- Code that needs type narrowing or error message verification\n- Functions where behavior is not easily testable through integration or browser tests\n\n**Avoid** unit tests when:\n\n- The function has external dependencies (database, API calls)\n- The behavior is better verified through integration tests\n- The function is simple enough that bugs would be caught by TypeScript\n\n---\n\n## Writing Unit Tests\n\nCreate test files with the `.test.ts` suffix. Bun automatically discovers and runs them:\n\n```typescript\nimport { describe, it, expect } from \"bun:test\";\n\ndescribe(\"myFunction\", () => {\n  it(\"returns expected value\", () => {\n    expect(myFunction()).toBe(\"expected\");\n  });\n});\n```\n\n---\n\n## Example: Testing an Assertion Helper\n\nHere's a complete test file for an assertion utility at `src/lib/common/assert.test.ts`:\n\n```typescript\nimport { describe, it, expect, mock } from \"bun:test\";\nimport assert from \"./assert\";\n\ndescribe(\"assert\", () => {\n  describe(\"truthy conditions\", () => {\n    it(\"does not throw for true\", () => {\n      expect(() => assert(true)).not.toThrow();\n    });\n\n    it(\"does not throw for truthy values\", () => {\n      expect(() => assert(1)).not.toThrow();\n      expect(() => assert(\"string\")).not.toThrow();\n      expect(() => assert({})).not.toThrow();\n      expect(() => assert([])).not.toThrow();\n    });\n  });\n\n  describe(\"falsy conditions\", () => {\n    it(\"throws for false\", () => {\n      expect(() => assert(false)).toThrow();\n    });\n\n    it(\"throws for null\", () => {\n      expect(() => assert(null)).toThrow();\n    });\n\n    it(\"throws for undefined\", () => {\n      expect(() => assert(undefined)).toThrow();\n    });\n  });\n\n  describe(\"error messages\", () => {\n    it(\"uses default message when none provided\", () => {\n      try {\n        assert(false);\n        expect.unreachable(\"Should have thrown\");\n      } catch (e) {\n        expect((e as Error).message).toBe(\"Assertion failed\");\n      }\n    });\n\n    it(\"uses string message when provided\", () => {\n      try {\n        assert(false, \"Custom error message\");\n        expect.unreachable(\"Should have thrown\");\n      } catch (e) {\n        expect((e as Error).message).toBe(\n          \"Assertion failed: Custom error message\",\n        );\n      }\n    });\n\n    it(\"calls lazy function for message\", () => {\n      const messageFn = mock(() => \"Lazy message\");\n\n      try {\n        assert(false, messageFn);\n        expect.unreachable(\"Should have thrown\");\n      } catch (e) {\n        expect(messageFn).toHaveBeenCalled();\n        expect((e as Error).message).toBe(\"Assertion failed: Lazy message\");\n      }\n    });\n\n    it(\"does not call lazy function when condition is truthy\", () => {\n      const messageFn = mock(() => \"Should not be called\");\n\n      assert(true, messageFn);\n\n      expect(messageFn).not.toHaveBeenCalled();\n    });\n  });\n\n  describe(\"type narrowing\", () => {\n    it(\"narrows nullable types\", () => {\n      const value: string | null = \"hello\";\n      assert(value !== null);\n      expect(value.toUpperCase()).toBe(\"HELLO\");\n    });\n  });\n});\n```\n\n---\n\n## Testing Patterns\n\n**1. Testing thrown errors:**\n\n```typescript\nexpect(() => throwingFunction()).toThrow(ErrorClass);\n\n// Or with try/catch for message inspection\ntry {\n  throwingFunction();\n  expect.unreachable(\"Should have thrown\");\n} catch (e) {\n  expect((e as Error).message).toContain(\"expected text\");\n}\n```\n\n**2. Using mocks:**\n\n```typescript\nimport { mock } from \"bun:test\";\n\nconst mockFn = mock(() => \"return value\");\nexpect(mockFn).toHaveBeenCalled();\nexpect(mockFn).not.toHaveBeenCalled();\n```\n\n**3. Testing environment variables:**\n\n```typescript\nlet originalEnv: NodeJS.ProcessEnv;\n\nbeforeEach(() => {\n  originalEnv = { ...process.env };\n});\n\nafterEach(() => {\n  process.env = originalEnv;\n});\n```\n\n**4. Simulating client environment:**\n\n```typescript\n// @ts-expect-error - intentionally manipulating global for tests\nglobalThis.window = {};\n\n// Clean up in afterEach\n// @ts-expect-error\ndelete globalThis.window;\n```\n\n---\n\n## Running Unit Tests\n\n```bash\nbun run test:unit               # Run unit tests with isolated Neon branch\nbun test src                    # Run all unit tests directly\nbun test src/lib/common         # Run tests in a directory\nbun test src/lib/common/assert  # Run a specific test file\n```\n\n---\n\n## CI with GitHub Actions\n\nRun tests automatically on every push:\n\n```yaml\n# .github/workflows/test.yml\nname: Test\n\non:\n  push:\n    branches:\n      - \"**\"\n  pull_request:\n    branches:\n      - \"**\"\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup Bun\n        uses: oven-sh/setup-bun@v2\n        with:\n          bun-version: latest\n\n      - name: Install dependencies\n        run: bun install --frozen-lockfile\n\n      - name: Run unit tests\n        run: bun test src\n```"}