Merge pull request #11 from stevedylandev/feat/add-tests d4b9f1d1
feat/add tests
Steve Simkins · 2025-08-02 15:43 9 file(s) · +377 −1
.github/workflows/test-cli-options.yml (added) +166 −0
1 +
name: Test CLI Options
2 +
3 +
on:
4 +
  push:
5 +
    branches: [ main ]
6 +
  pull_request:
7 +
    branches: [ main ]
8 +
9 +
jobs:
10 +
  test-cli-options:
11 +
    runs-on: ubuntu-latest
12 +
    
13 +
    strategy:
14 +
      fail-fast: false
15 +
      matrix:
16 +
        include:
17 +
          # Default template combinations
18 +
          - template: "default"
19 +
            rpc: true
20 +
            linter: "eslint"
21 +
            test_name: "Default + RPC + ESLint"
22 +
          - template: "default"
23 +
            rpc: true
24 +
            linter: "biome"
25 +
            test_name: "Default + RPC + Biome"
26 +
          - template: "default"
27 +
            rpc: false
28 +
            linter: "eslint"
29 +
            test_name: "Default + No RPC + ESLint"
30 +
          - template: "default"
31 +
            rpc: false
32 +
            linter: "biome"
33 +
            test_name: "Default + No RPC + Biome"
34 +
          
35 +
          # Tailwind template combinations
36 +
          - template: "tailwind"
37 +
            rpc: true
38 +
            linter: "eslint"
39 +
            test_name: "Tailwind + RPC + ESLint"
40 +
          - template: "tailwind"
41 +
            rpc: true
42 +
            linter: "biome"
43 +
            test_name: "Tailwind + RPC + Biome"
44 +
          - template: "tailwind"
45 +
            rpc: false
46 +
            linter: "eslint"
47 +
            test_name: "Tailwind + No RPC + ESLint"
48 +
          - template: "tailwind"
49 +
            rpc: false
50 +
            linter: "biome"
51 +
            test_name: "Tailwind + No RPC + Biome"
52 +
          
53 +
          # Shadcn template combinations
54 +
          - template: "shadcn"
55 +
            rpc: true
56 +
            linter: "eslint"
57 +
            test_name: "Shadcn + RPC + ESLint"
58 +
          - template: "shadcn"
59 +
            rpc: true
60 +
            linter: "biome"
61 +
            test_name: "Shadcn + RPC + Biome"
62 +
          - template: "shadcn"
63 +
            rpc: false
64 +
            linter: "eslint"
65 +
            test_name: "Shadcn + No RPC + ESLint"
66 +
          - template: "shadcn"
67 +
            rpc: false
68 +
            linter: "biome"
69 +
            test_name: "Shadcn + No RPC + Biome"
70 +
71 +
    steps:
72 +
      - name: Checkout repository
73 +
        uses: actions/checkout@v4
74 +
75 +
      - name: Setup Bun
76 +
        uses: oven-sh/setup-bun@v2
77 +
        with:
78 +
          bun-version: latest
79 +
80 +
      - name: Install dependencies
81 +
        run: bun install
82 +
83 +
      - name: Build CLI
84 +
        run: bun run build
85 +
86 +
      - name: Create test project - ${{ matrix.test_name }}
87 +
        run: |
88 +
          # Create project with specified options
89 +
          echo "Creating project with options:"
90 +
          echo "Template: ${{ matrix.template }}"
91 +
          echo "RPC: ${{ matrix.rpc }}"
92 +
          echo "Linter: ${{ matrix.linter }}"
93 +
          
94 +
          if [ "${{ matrix.rpc }}" = "true" ]; then
95 +
            ./dist/index.js test-project-${{ matrix.template }}-${{ matrix.rpc }}-${{ matrix.linter }} \
96 +
              --yes \
97 +
              --template ${{ matrix.template }} \
98 +
              --rpc \
99 +
              --linter ${{ matrix.linter }}
100 +
          else
101 +
            ./dist/index.js test-project-${{ matrix.template }}-${{ matrix.rpc }}-${{ matrix.linter }} \
102 +
              --yes \
103 +
              --template ${{ matrix.template }} \
104 +
              --linter ${{ matrix.linter }}
105 +
          fi
106 +
107 +
      - name: Install project dependencies
108 +
        run: |
109 +
          cd test-project-${{ matrix.template }}-${{ matrix.rpc }}-${{ matrix.linter }}
110 +
          bun install
111 +
112 +
      - name: Build test project
113 +
        run: |
114 +
          cd test-project-${{ matrix.template }}-${{ matrix.rpc }}-${{ matrix.linter }}
115 +
          bun run build
116 +
117 +
      - name: Verify build outputs
118 +
        run: |
119 +
          cd test-project-${{ matrix.template }}-${{ matrix.rpc }}-${{ matrix.linter }}
120 +
          
121 +
          # Check that dist directories exist
122 +
          if [ ! -d "client/dist" ]; then
123 +
            echo "❌ Client build failed - dist directory not found"
124 +
            exit 1
125 +
          fi
126 +
          
127 +
          if [ ! -d "server/dist" ]; then
128 +
            echo "❌ Server build failed - dist directory not found"
129 +
            exit 1
130 +
          fi
131 +
          
132 +
          # Check for expected files
133 +
          if [ ! -f "client/dist/index.html" ]; then
134 +
            echo "❌ Client build incomplete - index.html not found"
135 +
            exit 1
136 +
          fi
137 +
          
138 +
          if [ ! -f "server/dist/index.js" ]; then
139 +
            echo "❌ Server build incomplete - index.js not found"
140 +
            exit 1
141 +
          fi
142 +
          
143 +
          echo "✅ Build verification passed for ${{ matrix.test_name }}"
144 +
145 +
      - name: Run linter on generated project
146 +
        run: |
147 +
          cd test-project-${{ matrix.template }}-${{ matrix.rpc }}-${{ matrix.linter }}
148 +
          
149 +
          if [ "${{ matrix.linter }}" = "eslint" ]; then
150 +
            # Check if ESLint config exists and run it
151 +
            if [ -f ".eslintrc.json" ] || [ -f ".eslintrc.js" ] || [ -f "eslint.config.js" ]; then
152 +
              echo "Running ESLint..."
153 +
              bun run lint || echo "ESLint warnings/errors found but continuing..."
154 +
            fi
155 +
          elif [ "${{ matrix.linter }}" = "biome" ]; then
156 +
            # Check if Biome config exists and run it
157 +
            if [ -f "biome.json" ]; then
158 +
              echo "Running Biome..."
159 +
              bun run lint || echo "Biome warnings/errors found but continuing..."
160 +
            fi
161 +
          fi
162 +
163 +
      - name: Cleanup test project
164 +
        if: always()
165 +
        run: |
166 +
          rm -rf test-project-${{ matrix.template }}-${{ matrix.rpc }}-${{ matrix.linter }}
.github/workflows/test.yml (added) +29 −0
1 +
name: Test
2 +
3 +
on:
4 +
  push:
5 +
    branches: [ main ]
6 +
  pull_request:
7 +
    branches: [ main ]
8 +
9 +
jobs:
10 +
  test:
11 +
    runs-on: ubuntu-latest
12 +
13 +
    steps:
14 +
      - name: Checkout repository
15 +
        uses: actions/checkout@v4
16 +
17 +
      - name: Setup Bun
18 +
        uses: oven-sh/setup-bun@v2
19 +
        with:
20 +
          bun-version: latest
21 +
22 +
      - name: Install dependencies
23 +
        run: bun install
24 +
25 +
      - name: Run tests
26 +
        run: bun test
27 +
28 +
      - name: Run tests with coverage
29 +
        run: bun test --coverage
TESTING.md (added) +60 −0
1 +
# Testing Guide
2 +
3 +
This project uses [Bun Test](https://bun.sh/docs/cli/test) for focused, high-value testing of core business logic.
4 +
5 +
## Running Tests
6 +
7 +
```bash
8 +
# Run all tests
9 +
bun test
10 +
11 +
# Run tests in watch mode (for development)
12 +
bun test --watch
13 +
14 +
# Run tests with coverage report
15 +
bun test --coverage
16 +
```
17 +
18 +
## Test Philosophy
19 +
20 +
This project follows a **focused testing approach** that prioritizes:
21 +
22 +
1. **High Value**: Test core business logic and data validation
23 +
2. **Fast Execution**: No slow I/O operations or complex mocking
24 +
3. **Maintainability**: Simple, reliable tests that won't break with dependencies
25 +
4. **Clarity**: Each test has a clear purpose and validates real behavior
26 +
27 +
## What We Test ✅
28 +
29 +
### **Core Utilities** (100% Coverage)
30 +
- `src/utils/try-catch.test.ts` - Error handling wrapper functionality
31 +
- `src/utils/templates.test.ts` - Template configuration and Hono code generation
32 +
- `src/utils/constants.test.ts` - Application constants validation
33 +
- `src/types.test.ts` - TypeScript type definitions and interfaces
34 +
35 +
## What We DON'T Test ❌
36 +
37 +
**CLI Functions with External Dependencies:**
38 +
- File system operations (degit, fs-extra)
39 +
- Interactive prompts (consola)
40 +
- System commands (git, bun install)
41 +
- Process management (process.exit)
42 +
43 +
**Why:** These functions are integration points with the OS and external tools. Testing them would require complex mocking that provides little value and high maintenance overhead.
44 +
45 +
## Test Configuration
46 +
47 +
### `bunfig.toml`
48 +
```toml
49 +
[test]
50 +
coverage = true
51 +
timeout = 5000
52 +
53 +
[test.env]
54 +
NODE_ENV = "test"
55 +
```
56 +
57 +
### Package Scripts
58 +
- `bun test` - Run all tests
59 +
- `bun test --watch` - Watch mode for development
60 +
- `bun test --coverage` - Generate coverage reports
bunfig.toml (added) +8 −0
1 +
[test]
2 +
# Test configuration for Bun
3 +
coverage = true
4 +
timeout = 5000
5 +
6 +
# Environment variables for tests
7 +
[test.env]
8 +
NODE_ENV = "test"
package.json +4 −1
13 13
  ],
14 14
  "scripts": {
15 15
    "build": "bun build src/index.ts --outdir dist --target node",
16 -
    "start": "bun ./dist/index.js"
16 +
    "start": "bun ./dist/index.js",
17 +
    "test": "bun test",
18 +
    "test:watch": "bun test --watch",
19 +
    "test:coverage": "bun test --coverage"
17 20
  },
18 21
  "keywords": [
19 22
    "bun",
src/types.test.ts (added) +28 −0
1 +
import { describe, it, expect } from "bun:test";
2 +
import type { TemplateInfo, ProjectOptions, ProjectResult } from "./types";
3 +
4 +
describe("TypeScript types", () => {
5 +
  it("should define correct type structures", () => {
6 +
    // Test that types can be instantiated correctly
7 +
    const templateInfo: TemplateInfo = {
8 +
      branch: "main",
9 +
      description: "Test template"
10 +
    };
11 +
12 +
    const options: ProjectOptions = {
13 +
      projectName: "test-project",
14 +
      linter: "biome"
15 +
    };
16 +
17 +
    const result: ProjectResult = {
18 +
      projectName: "test-project",
19 +
      gitInitialized: true,
20 +
      dependenciesInstalled: false,
21 +
      template: "default"
22 +
    };
23 +
24 +
    expect(templateInfo.branch).toBe("main");
25 +
    expect(options.linter).toBe("biome");
26 +
    expect(result.projectName).toBe("test-project");
27 +
  });
28 +
});
src/utils/constants.test.ts (added) +16 −0
1 +
import { describe, it, expect } from "bun:test";
2 +
import { DEFAULT_REPO } from "./constants";
3 +
4 +
describe("constants", () => {
5 +
  it("should have correct default repository", () => {
6 +
    expect(DEFAULT_REPO).toBe("stevedylandev/bhvr");
7 +
  });
8 +
9 +
  it("should be a string", () => {
10 +
    expect(typeof DEFAULT_REPO).toBe("string");
11 +
  });
12 +
13 +
  it("should follow GitHub repo format", () => {
14 +
    expect(DEFAULT_REPO).toMatch(/^[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+$/);
15 +
  });
16 +
});
src/utils/templates.test.ts (added) +25 −0
1 +
import { describe, it, expect } from "bun:test";
2 +
import { TEMPLATES, honoRpcTemplate, honoClientTemplate } from "./templates";
3 +
4 +
describe("Templates", () => {
5 +
	it("should have all required templates with correct structure", () => {
6 +
		const expectedTemplates = ["default", "tailwind", "shadcn"];
7 +
8 +
		expectedTemplates.forEach((templateName) => {
9 +
			expect(TEMPLATES).toHaveProperty(templateName);
10 +
			const template = TEMPLATES[templateName];
11 +
			expect(template).toHaveProperty("branch");
12 +
			expect(template).toHaveProperty("description");
13 +
			expect(typeof template?.branch).toBe("string");
14 +
			expect(typeof template?.description).toBe("string");
15 +
		});
16 +
	});
17 +
18 +
	it("should have valid Hono templates", () => {
19 +
		expect(honoRpcTemplate).toContain("import { Hono }");
20 +
		expect(honoRpcTemplate).toContain("export const app = new Hono()");
21 +
22 +
		expect(honoClientTemplate).toContain("import { hc }");
23 +
		expect(honoClientTemplate).toContain("export const hcWithType");
24 +
	});
25 +
});
src/utils/try-catch.test.ts (added) +41 −0
1 +
import { describe, it, expect } from "bun:test";
2 +
import { tryCatch } from "./try-catch";
3 +
4 +
describe("tryCatch", () => {
5 +
  it("should return success result for resolved promise", async () => {
6 +
    const result = await tryCatch(Promise.resolve("success"));
7 +
    
8 +
    expect(result.data).toBe("success");
9 +
    expect(result.error).toBeNull();
10 +
  });
11 +
12 +
  it("should return failure result for rejected promise", async () => {
13 +
    const error = new Error("test error");
14 +
    const result = await tryCatch(Promise.reject(error));
15 +
    
16 +
    expect(result.data).toBeNull();
17 +
    expect(result.error).toBe(error);
18 +
  });
19 +
20 +
  it("should handle async function that throws", async () => {
21 +
    const asyncFunction = async () => {
22 +
      throw new Error("async error");
23 +
    };
24 +
    
25 +
    const result = await tryCatch(asyncFunction());
26 +
    
27 +
    expect(result.data).toBeNull();
28 +
    expect(result.error).toBeInstanceOf(Error);
29 +
    expect((result.error as Error).message).toBe("async error");
30 +
  });
31 +
32 +
  it("should handle different data types", async () => {
33 +
    const objectResult = await tryCatch(Promise.resolve({ id: 1, name: "test" }));
34 +
    const numberResult = await tryCatch(Promise.resolve(42));
35 +
    const booleanResult = await tryCatch(Promise.resolve(true));
36 +
    
37 +
    expect(objectResult.data).toEqual({ id: 1, name: "test" });
38 +
    expect(numberResult.data).toBe(42);
39 +
    expect(booleanResult.data).toBe(true);
40 +
  });
41 +
});