Documentation
¶
Overview ¶
Package testutil provides testing utilities and helpers for Hitch's test suite.
Overview ¶
This package implements a comprehensive testing infrastructure that enables isolated, reproducible testing of Git operations. It provides test harnesses, mock repositories, and helper functions that make writing tests easier and more reliable.
Core Purpose ¶
The package serves several key purposes:
- Create isolated Git repositories for testing
- Provide helper methods for common test operations
- Ensure tests don't interfere with each other
- Enable isolated integration testing
- Simplify test setup and teardown
TestRepo Type ¶
The primary type is TestRepo, which represents an isolated test repository:
type TestRepo struct {
Path string // Temporary directory path
Repo *git.Repo // Hitch's Repo wrapper
T *testing.T // Test instance
GoGit *git.Repository // Underlying go-git repository
}
Creating Test Repositories ¶
Create a fresh, isolated repository for each test:
func TestMyFeature(t *testing.T) {
testRepo := testutil.NewTestRepo(t)
defer testRepo.Cleanup()
// Test repository is ready to use
// - Has main branch
// - Has initial commit
// - Git user configured
// - Clean working directory
// Run your tests...
}
The test repository is created in a temporary directory and is completely isolated from other tests and the host system.
Initial Repository State ¶
NewTestRepo creates a repository with:
- Initialized git repository (git init)
- Default branch set to 'main'
- Git user configured (Test User <test@example.com>)
- GPG signing disabled (for faster commits)
- Initial commit with README.md
- Clean working directory
Helper Methods ¶
TestRepo provides many helper methods for common operations:
Branch Operations:
// Create and optionally switch to branch
err := testRepo.CreateBranch("feature/test", false)
// Switch to existing branch
err := testRepo.Checkout("feature/test")
// Check if branch exists
exists := testRepo.BranchExists("feature/test")
Commit Operations:
// Create a file and commit it
err := testRepo.CommitFile("test.txt", "content", "Add test file")
// Commit multiple files
files := map[string]string{
"file1.txt": "content1",
"file2.txt": "content2",
}
err := testRepo.CommitFiles(files, "Add multiple files")
Metadata Operations:
// Initialize Hitch metadata
meta := testRepo.InitializeMetadata([]string{"dev", "qa"}, "main")
// Write metadata
err := testRepo.WriteMetadata(meta, "Update metadata")
Cleanup ¶
Always clean up test repositories:
testRepo := testutil.NewTestRepo(t) defer testRepo.Cleanup()
Cleanup:
- Removes the temporary directory
- Deletes all files and git data
- Frees disk space
- Prevents test pollution
Using defer ensures cleanup happens even if test panics.
Complete Test Example ¶
func TestPromoteOperation(t *testing.T) {
// Create isolated repository
testRepo := testutil.NewTestRepo(t)
defer testRepo.Cleanup()
// Initialize metadata
meta := metadata.NewMetadata([]string{"dev"}, "main", "test@example.com")
writer := metadata.NewWriter(testRepo.Repo)
err := writer.WriteInitial(meta, "Test User", "test@example.com")
if err != nil {
t.Fatalf("Failed to initialize metadata: %v", err)
}
// Create feature branch
err = testRepo.CreateBranch("feature/login", true)
if err != nil {
t.Fatalf("Failed to create branch: %v", err)
}
// Make changes
err = testRepo.CommitFile("login.go", "package main", "Add login feature")
if err != nil {
t.Fatalf("Failed to commit: %v", err)
}
// Switch back to main
err = testRepo.Checkout("main")
if err != nil {
t.Fatalf("Failed to checkout main: %v", err)
}
// Test promote operation
err = meta.AddBranchToEnvironment("dev", "feature/login", "test@example.com")
if err != nil {
t.Errorf("Promote failed: %v", err)
}
// Verify
if !meta.IsBranchInEnvironment("dev", "feature/login") {
t.Error("Branch should be in dev environment")
}
}
Local Testing ¶
Tests using testutil run in isolated repositories and can be executed locally:
go test ./...
The RunInTestRepo helper ensures that Hitch commands run in isolated test repositories, preventing repository poisoning of the current directory.
Each test gets its own isolated environment:
- Clean Git repository
- Temporary directory
- No host system interference
- Reproducible results
🚨 CRITICAL: Repository Safety ¶
**NEVER run Hitch commands directly in tests without using RunInTestRepo()!**
Hitch commands that manipulate Git state MUST be wrapped in RunInTestRepo() to prevent poisoning the Hitch development repository.
❌ WRONG - This will poison your Hitch repo:
func TestBadExample(t *testing.T) {
// This runs in the current directory (Hitch repo!)
err := runPromote(promoteCmd, []string{"feature/test", "to", "dev"})
// ^^^ BAD: Modifies the Hitch repo itself!
}
✅ CORRECT - This runs in an isolated repo:
func TestGoodExample(t *testing.T) {
testRepo := testutil.NewTestRepo(t)
defer testRepo.Cleanup()
// Run Hitch commands in isolated repository
err := testRepo.RunInTestRepo(func() {
promoteNoRebuild = true
if err := runPromote(promoteCmd, []string{"feature/test", "to", "dev"}); err != nil {
t.Errorf("Promote failed: %v", err)
}
})
if err != nil {
t.Fatalf("Failed to run Hitch command in test repo: %v", err)
}
// ^^^ GOOD: Runs in temporary directory, Hitch repo stays clean!
}
📝 Test Writing Guidelines ¶
## When to Use RunInTestRepo()
ALWAYS use RunInTestRepo() for ANY test that calls:
- Hitch command functions (runPromote, runRebuild, runStatus, etc.) - Any function that calls Hitch commands - Direct git operations that could affect the current repo - Metadata operations that modify the repository
## Safe Test Patterns
### Unit Tests (No RunInTestRepo needed)
func TestValidatorLogic(t *testing.T) {
validator := security.NewSecurityValidator()
result := validator.ValidateBranchName("feature/test")
// ✅ SAFE: Pure logic, no repo operations
}
### Integration Tests with Hitch Commands (RunInTestRepo REQUIRED)
func TestPromoteWorkflow(t *testing.T) {
testRepo := testutil.NewTestRepo(t)
defer testRepo.Cleanup()
// ✅ SAFE: Hitch commands run in isolated repo
err := testRepo.RunInTestRepo(func() {
promoteNoRebuild = true
if err := runPromote(promoteCmd, []string{"feature/test", "to", "dev"}); err != nil {
t.Errorf("Promote failed: %v", err)
}
})
if err != nil {
t.Fatalf("Failed to run Hitch command in test repo: %v", err)
}
}
### Error Handling Patterns
For tests that expect Hitch commands to fail:
err := testRepo.RunInTestRepo(func() {
promoteNoRebuild = true
if err := runPromote(promoteCmd, []string{"invalid-branch", "to", "dev"}); err == nil {
t.Error("Expected promote to fail")
}
})
if err != nil {
t.Fatalf("Failed to run Hitch command in test repo: %v", err)
}
For tests that need to capture and inspect errors:
var promoteErr error
err := testRepo.RunInTestRepo(func() {
promoteNoRebuild = true
promoteErr = runPromote(promoteCmd, []string{"feature/test", "to", "dev"})
})
if err != nil {
t.Fatalf("Failed to run Hitch command in test repo: %v", err)
}
if promoteErr != nil {
t.Logf("Promote failed as expected: %v", promoteErr)
}
🛡️ What RunInTestRepo() Protects Against ¶
Without RunInTestRepo(), Hitch tests can:
- Create branches in the Hitch development repo - Modify .hitch metadata files - Change current working directory - Leave uncommitted changes - Create lock files - Corrupt the development environment
RunInTestRepo() prevents ALL of these by:
1. **Directory Isolation**: Changes to a temporary directory 2. **Automatic Cleanup**: Deletes the temp directory when test finishes 3. **Directory Restoration**: Always returns to original directory 4. **Panic Safety**: Cleanup runs even if test panics
🧪 Running Tests ¶
## Local Development ```bash # Run all tests locally go test ./...
# Run specific package tests go test -v ./internal/security
# Run with coverage go test -coverprofile=coverage.out ./... go tool cover -html=coverage.out ```
## CI/CD Tests run the same way locally and in CI.
Test Isolation ¶
TestRepo ensures perfect test isolation:
- Each test gets its own temporary directory
- No shared state between tests
- Tests can run in parallel
- No leftover files after tests
Enable parallel tests:
func TestSomething(t *testing.T) {
t.Parallel()
testRepo := testutil.NewTestRepo(t)
defer testRepo.Cleanup()
// Test runs in parallel with others
}
Helper Functions ¶
Additional helper functions:
File operations:
// Write file to repository
err := testRepo.WriteFile("path/to/file.txt", "content")
// Read file from repository
content, err := testRepo.ReadFile("path/to/file.txt")
Git operations:
// Get current commit SHA
sha, err := testRepo.CurrentCommit()
// Get commit count
count, err := testRepo.CommitCount()
// Check if file exists
exists := testRepo.FileExists("test.txt")
Test Patterns ¶
Common test patterns using testutil:
Table-Driven Tests:
func TestBranchOperations(t *testing.T) {
testCases := []struct {
name string
branch string
expected bool
}{
{"valid branch", "feature/test", true},
{"invalid branch", "feat/test;", false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
testRepo := testutil.NewTestRepo(t)
defer testRepo.Cleanup()
// Test with isolated repository
})
}
}
Setup/Teardown Pattern:
func TestWithSetup(t *testing.T) {
testRepo := testutil.NewTestRepo(t)
defer testRepo.Cleanup()
// Setup
setupTestData(testRepo)
// Execute test
runTest(testRepo)
// Verify
verifyResults(testRepo)
// Cleanup happens automatically via defer
}
Performance Considerations ¶
Test repository creation is fast:
- Uses os.MkdirTemp for temporary directories
- Minimal git initialization
- No network operations
- Typical creation time: 10-50ms
For tests that need multiple repositories:
func TestMultiRepo(t *testing.T) {
repo1 := testutil.NewTestRepo(t)
defer repo1.Cleanup()
repo2 := testutil.NewTestRepo(t)
defer repo2.Cleanup()
// Test interactions between repositories
}
Debugging Tests ¶
When tests fail, TestRepo provides helpful information:
t.Logf("Test repository: %s", testRepo.Path)
t.Logf("Current branch: %s", testRepo.Repo.CurrentBranch())
t.Logf("Branches: %v", testRepo.Repo.Branches())
To inspect test repository after failure:
func TestDebug(t *testing.T) {
testRepo := testutil.NewTestRepo(t)
// Comment out cleanup to inspect repository
// defer testRepo.Cleanup()
// Test code...
t.Logf("Repository at: %s", testRepo.Path)
// Can cd to path and inspect git state
}
Best Practices ¶
- Always use defer for Cleanup()
- Create new TestRepo for each test (don't share)
- Use t.Helper() in helper functions
- Use t.Fatal for setup errors, t.Error for test failures
- Keep tests focused and isolated
- Use descriptive test names
- Clean up even if test fails
Error Handling ¶
TestRepo methods that can fail return errors:
err := testRepo.CreateBranch("feature/test", false)
if err != nil {
t.Fatalf("Setup failed: %v", err)
}
Use t.Fatalf for setup errors (stop test immediately) Use t.Errorf for test assertions (continue to see all failures)
Integration with Other Packages ¶
TestRepo integrates seamlessly with other Hitch packages:
// With metadata package meta := metadata.NewMetadata(envs, "main", "test@example.com") writer := metadata.NewWriter(testRepo.Repo) // With safety package tester := safety.NewTempBranchTester(testRepo.Repo) // With git package validator := git.NewRepositoryValidator(testRepo.Repo)
Test Coverage ¶
Using testutil enables high test coverage:
- Unit tests: Test individual functions
- Integration tests: Test component interactions
- End-to-end tests: Test complete workflows
Measure coverage:
go test -coverprofile=coverage.out ./... go tool cover -html=coverage.out
Example: Complex Integration Test ¶
func TestCompleteWorkflow(t *testing.T) {
// Create repository
testRepo := testutil.NewTestRepo(t)
defer testRepo.Cleanup()
// Initialize Hitch
meta := metadata.NewMetadata([]string{"dev", "qa"}, "main", "test@example.com")
writer := metadata.NewWriter(testRepo.Repo)
err := writer.WriteInitial(meta, "Test User", "test@example.com")
require.NoError(t, err)
// Create feature
err = testRepo.CreateBranch("feature/workflow", true)
require.NoError(t, err)
err = testRepo.CommitFile("feature.go", "package main", "Add feature")
require.NoError(t, err)
// Promote to dev
err = meta.AddBranchToEnvironment("dev", "feature/workflow", "test@example.com")
require.NoError(t, err)
// Promote to qa
err = meta.AddBranchToEnvironment("qa", "feature/workflow", "test@example.com")
require.NoError(t, err)
// Verify final state
assert.True(t, meta.IsBranchInEnvironment("dev", "feature/workflow"))
assert.True(t, meta.IsBranchInEnvironment("qa", "feature/workflow"))
}
This package is the foundation of Hitch's testing infrastructure, enabling comprehensive, reliable, and isolated testing of all Git operations.
Index ¶
- type TestRepo
- func (tr *TestRepo) BranchExists(name string) bool
- func (tr *TestRepo) Cleanup()
- func (tr *TestRepo) CommitFile(filename, content, message string) error
- func (tr *TestRepo) CreateBranch(name string, createCommit bool) error
- func (tr *TestRepo) CreateRemote(remoteName string) error
- func (tr *TestRepo) GetCurrentBranch() (string, error)
- func (tr *TestRepo) HasRemote(remoteName string) bool
- func (tr *TestRepo) PushBranch(remoteName, branchName string) error
- func (tr *TestRepo) ResetWorkingDirectory()
- func (tr *TestRepo) RunInTestRepo(testFunc func()) error
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type TestRepo ¶
type TestRepo struct {
Path string
Repo *hitchgit.Repo
T *testing.T
GoGit *git.Repository // Underlying go-git repository
RemotePath string // Path to bare remote repository if created
}
TestRepo represents an isolated Git repository for testing
func NewTestRepo ¶
NewTestRepo creates a new isolated Git repository in a temporary directory
func (*TestRepo) BranchExists ¶
BranchExists checks if a branch exists
func (*TestRepo) Cleanup ¶
func (tr *TestRepo) Cleanup()
Cleanup removes the test repository and remote if created
func (*TestRepo) CommitFile ¶
CommitFile creates a file and commits it to the current branch
func (*TestRepo) CreateBranch ¶
CreateBranch creates a new branch with an optional commit
func (*TestRepo) CreateRemote ¶ added in v1.1.10
CreateRemote creates a bare Git repository to act as a remote This simulates a private Git repository for testing push operations
func (*TestRepo) GetCurrentBranch ¶
GetCurrentBranch returns the current branch name
func (*TestRepo) PushBranch ¶ added in v1.1.10
PushBranch pushes a specific branch to the remote
func (*TestRepo) ResetWorkingDirectory ¶
func (tr *TestRepo) ResetWorkingDirectory()
ResetWorkingDirectory ensures the git working directory is clean and on the correct branch
func (*TestRepo) RunInTestRepo ¶
RunInTestRepo changes to the test repository directory and executes a function This ensures Hitch commands run in the isolated test repository instead of poisoning the current repo