Skip to content

Commit 8e5c9a5

Browse files
committed
feat: Add integration test generation (chu gen integration)
Generate integration tests for component interactions - Analyze package structure and components - Identify handlers, services, repositories - Detect dependencies between components - Generate tests with setup/teardown - Use //go:build integration tag - LLM creates end-to-end test scenarios Autonomy: 70% (45/64 scenarios) Test Generation: 7/8 complete
1 parent d4b076e commit 8e5c9a5

File tree

3 files changed

+323
-10
lines changed

3 files changed

+323
-10
lines changed

cmd/chu/main.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,11 @@ $0-5/month vs $20-30/month subscriptions.
5454
chu implement plan.md - Execute plan step-by-step
5555
5656
## SPECIALIZED TOOLS
57-
chu gen test <file> - Generate unit tests for a file
58-
chu gen mock <file> - Generate mocks for interfaces
59-
chu gen changelog - Generate CHANGELOG from git commits
60-
chu coverage [pkg] - Analyze test coverage gaps
57+
chu gen test <file> - Generate unit tests for a file
58+
chu gen integration <pkg> - Generate integration tests for a package
59+
chu gen mock <file> - Generate mocks for interfaces
60+
chu gen changelog - Generate CHANGELOG from git commits
61+
chu coverage [pkg] - Analyze test coverage gaps
6162
chu tdd - Test-driven development mode
6263
chu feature "desc" - Generate tests + implementation
6364
chu review [target] - Code review for bugs, security, improvements

docs/reference/capabilities.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -150,19 +150,19 @@ The following require human intervention:
150150

151151
---
152152

153-
### 🟡 Test Generation (6/8 scenarios)
153+
### 🟡 Test Generation (7/8 scenarios)
154154

155155
**Implemented:**
156156

157157
- ✅ Generate unit tests for new code (`chu gen test <file>`)
158+
- ✅ Generate integration tests (`chu gen integration <pkg>`)
158159
- ✅ Validate generated tests (compile + run)
159160
- ✅ Multi-language support (Go, TypeScript, Python)
160161
- ✅ Generate mock objects (`chu gen mock <file>`)
161162
- ✅ Identify coverage gaps (`chu coverage`)
162163

163164
**Not yet implemented:**
164165

165-
- Generate integration tests
166166
- Snapshot testing
167167

168168
**Example:**
@@ -173,7 +173,7 @@ chu gen test pkg/calculator/calculator.go
173173
```
174174

175175
**Limitations:**
176-
- Integration tests require coordinated setup
176+
- Integration tests currently Go-only
177177
- Mock generation currently Go-only
178178
- Coverage analysis currently Go-only
179179

@@ -240,11 +240,11 @@ chu gen changelog v1.0.0 # From v1.0.0 to HEAD
240240
- Database migrations
241241
- Type system improvements
242242

243-
**Phase 8: Test Generation (2 remaining scenarios)**
243+
**Phase 8: Test Generation (1 remaining scenario)**
244244
- ✅ Auto-generate unit tests for new code (DONE)
245+
- ✅ Integration test creation (DONE)
245246
- ✅ Mock generation (DONE)
246247
- ✅ Coverage gap identification (DONE)
247-
- Integration test creation
248248
- Snapshot testing
249249

250250
**Phase 9: Documentation (2 remaining scenarios)**
@@ -280,10 +280,11 @@ Skipped tests (t.Skip()) represent features not yet implemented.
280280
- ✅ CI failure handling
281281
- ✅ PR review iteration
282282
- ✅ Unit test generation
283+
- ✅ Integration test generation
283284
- ✅ Mock generation
284285
- ✅ Coverage gap identification
285286
- ✅ CHANGELOG generation
286-
- **Autonomy:** 44/64 (69%)
287+
- **Autonomy:** 45/64 (70%)
287288
- **MVAA Critical Path:** 17/17 (100%)
288289

289290
### Future Releases

internal/testgen/integration.go

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
package testgen
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"go/ast"
7+
"go/parser"
8+
"go/token"
9+
"os"
10+
"path/filepath"
11+
"strings"
12+
13+
"chuchu/internal/langdetect"
14+
"chuchu/internal/llm"
15+
)
16+
17+
type IntegrationTestGenerator struct {
18+
provider llm.Provider
19+
model string
20+
workDir string
21+
}
22+
23+
type Component struct {
24+
Name string
25+
File string
26+
Type string // handler, service, repository, client
27+
Dependencies []string
28+
Methods []string
29+
}
30+
31+
type IntegrationResult struct {
32+
TestFile string
33+
Valid bool
34+
Error error
35+
}
36+
37+
func NewIntegrationTestGenerator(provider llm.Provider, model, workDir string) *IntegrationTestGenerator {
38+
return &IntegrationTestGenerator{
39+
provider: provider,
40+
model: model,
41+
workDir: workDir,
42+
}
43+
}
44+
45+
func (g *IntegrationTestGenerator) GenerateIntegrationTests(ctx context.Context, packagePath string) (*IntegrationResult, error) {
46+
absPath := packagePath
47+
if !filepath.IsAbs(packagePath) {
48+
absPath = filepath.Join(g.workDir, packagePath)
49+
}
50+
51+
lang := g.detectLanguage(absPath)
52+
if lang != langdetect.Go {
53+
return nil, fmt.Errorf("integration test generation currently only supports Go")
54+
}
55+
56+
components, err := g.analyzeComponents(absPath)
57+
if err != nil {
58+
return nil, fmt.Errorf("failed to analyze components: %w", err)
59+
}
60+
61+
if len(components) == 0 {
62+
return nil, fmt.Errorf("no components found in %s", packagePath)
63+
}
64+
65+
testCode, err := g.generateIntegrationTestCode(ctx, components, absPath)
66+
if err != nil {
67+
return nil, fmt.Errorf("failed to generate test code: %w", err)
68+
}
69+
70+
testFile := filepath.Join(absPath, "integration_test.go")
71+
if err := os.WriteFile(testFile, []byte(testCode), 0644); err != nil {
72+
return nil, fmt.Errorf("failed to write test file: %w", err)
73+
}
74+
75+
result := &IntegrationResult{
76+
TestFile: testFile,
77+
Valid: true,
78+
}
79+
80+
validator := NewValidator(g.workDir, langdetect.Go)
81+
if !validator.Validate(testFile) {
82+
result.Valid = false
83+
result.Error = fmt.Errorf("validation failed")
84+
}
85+
86+
return result, nil
87+
}
88+
89+
func (g *IntegrationTestGenerator) detectLanguage(path string) langdetect.Language {
90+
detector := langdetect.NewDetector(path)
91+
breakdown, err := detector.Detect()
92+
if err != nil {
93+
return langdetect.Unknown
94+
}
95+
if breakdown.Primary == "" {
96+
return langdetect.Unknown
97+
}
98+
99+
switch breakdown.Primary {
100+
case "Go":
101+
return langdetect.Go
102+
case "TypeScript", "JavaScript":
103+
return langdetect.TypeScript
104+
case "Python":
105+
return langdetect.Python
106+
default:
107+
return langdetect.Unknown
108+
}
109+
}
110+
111+
func (g *IntegrationTestGenerator) analyzeComponents(pkgPath string) ([]Component, error) {
112+
var components []Component
113+
114+
err := filepath.Walk(pkgPath, func(path string, info os.FileInfo, err error) error {
115+
if err != nil {
116+
return err
117+
}
118+
119+
if info.IsDir() || !strings.HasSuffix(path, ".go") {
120+
return nil
121+
}
122+
123+
if strings.HasSuffix(path, "_test.go") {
124+
return nil
125+
}
126+
127+
comp, err := g.analyzeFile(path)
128+
if err != nil {
129+
return nil
130+
}
131+
132+
if comp != nil {
133+
components = append(components, *comp)
134+
}
135+
136+
return nil
137+
})
138+
139+
return components, err
140+
}
141+
142+
func (g *IntegrationTestGenerator) analyzeFile(filePath string) (*Component, error) {
143+
fset := token.NewFileSet()
144+
node, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments)
145+
if err != nil {
146+
return nil, err
147+
}
148+
149+
var comp *Component
150+
151+
ast.Inspect(node, func(n ast.Node) bool {
152+
switch decl := n.(type) {
153+
case *ast.GenDecl:
154+
if decl.Tok == token.TYPE {
155+
for _, spec := range decl.Specs {
156+
typeSpec, ok := spec.(*ast.TypeSpec)
157+
if !ok {
158+
continue
159+
}
160+
161+
structType, ok := typeSpec.Type.(*ast.StructType)
162+
if !ok {
163+
continue
164+
}
165+
166+
name := typeSpec.Name.Name
167+
compType := g.inferComponentType(name, structType)
168+
169+
if compType != "" {
170+
comp = &Component{
171+
Name: name,
172+
File: filePath,
173+
Type: compType,
174+
Dependencies: g.extractDependencies(structType),
175+
Methods: []string{},
176+
}
177+
}
178+
}
179+
}
180+
181+
case *ast.FuncDecl:
182+
if comp != nil && decl.Recv != nil {
183+
comp.Methods = append(comp.Methods, decl.Name.Name)
184+
}
185+
}
186+
return true
187+
})
188+
189+
return comp, nil
190+
}
191+
192+
func (g *IntegrationTestGenerator) inferComponentType(name string, structType *ast.StructType) string {
193+
nameLower := strings.ToLower(name)
194+
195+
if strings.Contains(nameLower, "handler") || strings.Contains(nameLower, "controller") {
196+
return "handler"
197+
}
198+
if strings.Contains(nameLower, "service") {
199+
return "service"
200+
}
201+
if strings.Contains(nameLower, "repository") || strings.Contains(nameLower, "store") {
202+
return "repository"
203+
}
204+
if strings.Contains(nameLower, "client") {
205+
return "client"
206+
}
207+
208+
for _, field := range structType.Fields.List {
209+
if len(field.Names) == 0 {
210+
continue
211+
}
212+
fieldName := strings.ToLower(field.Names[0].Name)
213+
if strings.Contains(fieldName, "db") || strings.Contains(fieldName, "store") {
214+
return "service"
215+
}
216+
}
217+
218+
return ""
219+
}
220+
221+
func (g *IntegrationTestGenerator) extractDependencies(structType *ast.StructType) []string {
222+
var deps []string
223+
224+
for _, field := range structType.Fields.List {
225+
if len(field.Names) == 0 {
226+
continue
227+
}
228+
229+
fieldType := g.exprToString(field.Type)
230+
if fieldType != "" && !isBasicType(fieldType) {
231+
deps = append(deps, fieldType)
232+
}
233+
}
234+
235+
return deps
236+
}
237+
238+
func (g *IntegrationTestGenerator) exprToString(expr ast.Expr) string {
239+
switch e := expr.(type) {
240+
case *ast.Ident:
241+
return e.Name
242+
case *ast.StarExpr:
243+
return g.exprToString(e.X)
244+
case *ast.SelectorExpr:
245+
return g.exprToString(e.X) + "." + e.Sel.Name
246+
default:
247+
return ""
248+
}
249+
}
250+
251+
func isBasicType(typ string) bool {
252+
basic := []string{"string", "int", "int64", "float64", "bool", "byte", "rune"}
253+
for _, b := range basic {
254+
if typ == b {
255+
return true
256+
}
257+
}
258+
return false
259+
}
260+
261+
func (g *IntegrationTestGenerator) generateIntegrationTestCode(ctx context.Context, components []Component, pkgPath string) (string, error) {
262+
pkgName := filepath.Base(pkgPath)
263+
264+
var compDescriptions []string
265+
for _, comp := range components {
266+
desc := fmt.Sprintf("- %s (%s): %v", comp.Name, comp.Type, comp.Methods)
267+
if len(comp.Dependencies) > 0 {
268+
desc += fmt.Sprintf("\n Dependencies: %v", comp.Dependencies)
269+
}
270+
compDescriptions = append(compDescriptions, desc)
271+
}
272+
273+
prompt := fmt.Sprintf(`Generate Go integration tests for these components:
274+
275+
Package: %s
276+
277+
Components:
278+
%s
279+
280+
Create integration tests that:
281+
1. Test interactions between components (not isolated units)
282+
2. Use real dependencies where possible, test doubles where needed
283+
3. Include setup/teardown for resources
284+
4. Test complete workflows end-to-end
285+
5. Handle errors and edge cases
286+
6. Use table-driven tests where appropriate
287+
288+
Requirements:
289+
- Package name: %s
290+
- Build tag: //go:build integration
291+
- Use testing.T
292+
- Include TestMain for setup/teardown if needed
293+
- Add cleanup with t.Cleanup()
294+
- Clear test names describing scenarios
295+
296+
Return ONLY the complete Go test code, no explanations.`, pkgName, strings.Join(compDescriptions, "\n"), pkgName)
297+
298+
resp, err := g.provider.Chat(ctx, llm.ChatRequest{
299+
UserPrompt: prompt,
300+
Model: g.model,
301+
})
302+
303+
if err != nil {
304+
return "", err
305+
}
306+
307+
code := strings.TrimSpace(resp.Text)
308+
code = extractCode(code)
309+
310+
return code, nil
311+
}

0 commit comments

Comments
 (0)