Module 3: Branching and Switching: Isolated Test Development
Learn to create and manage branches for developing new test features, fixing bugs, and experimenting with test frameworks without affecting the main test suite. Understand branch strategies specific to test automation projects and practice switching between different testing contexts.
Advanced Branch Context Switching for Test Engineers
Why This Matters
As a test automation engineer, you’re constantly juggling multiple priorities: developing new test scenarios for upcoming features, fixing flaky tests in the current suite, experimenting with new testing frameworks, and addressing urgent production bugs. Each of these tasks requires a different testing contextโdifferent dependencies, configurations, and often different versions of the application under test.
The problem: Switching between these contexts in a single codebase creates chaos. You might be halfway through writing a complex end-to-end test when an urgent bug report arrives. Simply abandoning your work or hastily committing incomplete code leads to broken test suites, confused team members, and lost productivity.
Real-world scenarios where advanced branch switching is essential:
- Emergency bug verification: A critical production bug is reported, and you need to immediately switch from developing new tests to creating a reproduction test on the stable branch
- Framework experimentation: You’re evaluating whether to migrate from Selenium to Playwright, but can’t afford to disrupt the existing test suite
- Parallel feature testing: Multiple developers are working on different features simultaneously, each requiring dedicated test coverage
- Environment-specific testing: Different branches maintain different configuration files for testing across dev, staging, and production environments
- Code review context: You need to check out a colleague’s test branch for review while preserving your in-progress work
Common pain points this lesson addresses:
- Losing uncommitted test code when switching branches unexpectedly
- Merge conflicts in test configuration files and dependencies
- Confusion about which tests belong to which features
- Inability to quickly pivot between testing priorities
- Test suite instability from mixing different testing contexts
- Time wasted recreating test environments after context switches
By mastering advanced branch context switching, you’ll maintain productivity, preserve work-in-progress, and seamlessly navigate between different testing prioritiesโall while keeping your test suite stable and your team’s workflow smooth.
What You’ll Accomplish
This lesson transforms you from someone who treats Git branches as a simple versioning tool into an expert who leverages branches as powerful testing contexts. By the end of this lesson, you’ll be able to confidently manage complex, real-world test automation scenarios that require rapid context switching.
Here’s how we’ll address each learning objective:
1. Advanced Branch Switching Techniques
You’ll move beyond basic git checkout
to master rapid context switching using modern Git commands. We’ll explore git switch
, understand the difference between switching branches and detaching HEAD states, and practice switching while carrying changes forward or leaving them behind. You’ll learn safety checks to prevent accidental data loss during switches.
2. Test-Specific Branch Naming and Organization
We’ll establish professional naming conventions tailored for test automation projects (e.g., test/feature-login
, bugfix/flaky-checkout-test
, experiment/playwright-migration
). You’ll create a branching strategy that makes it immediately clear what type of testing work exists on each branch, enabling your entire QA team to navigate the repository efficiently.
3. Git Stash for Preserving Incomplete Work
You’ll master git stash
as your safety net for context switching. Learn to stash incomplete test files, apply stashes to different branches, manage multiple stashes with descriptive names, and recover from stash conflicts. We’ll cover advanced techniques like partial stashing and stash popping versus applying.
4. Parallel Test Development Workflows
Through hands-on exercises, you’ll practice maintaining multiple active test development efforts simultaneously. You’ll create branches for a new feature test, a bug reproduction test, and a framework experimentโthen practice switching between them smoothly while keeping each context isolated and functional.
5. Resolving Branch Switching Conflicts
Finally, you’ll tackle the trickiest part: conflicts that arise from different test dependencies, configuration files, and package versions across branches. You’ll learn to identify why conflicts occur during switches, resolve them efficiently, and structure your test projects to minimize future conflicts.
Each objective includes practical, hands-on examples using realistic test automation scenarios, so you’ll build muscle memory for these workflows that you’ll use daily in your testing career.
Core Content
Advanced Branch Context Switching for Test Engineers
Core Concepts Explained
Understanding Branch Context in Test Automation
Branch context switching is a critical skill for test engineers working in continuous integration environments. When you switch branches in a Git repository, your test automation framework must adapt to potentially different:
- Test suites and test cases that may exist only on specific branches
- Dependencies and package versions that vary between feature branches
- Configuration files tailored to different environments or features
- Test data and fixtures specific to branch functionality
The key challenge is maintaining test integrity while rapidly switching between development contexts without cross-contamination of test artifacts or configurations.
The Branch Switching Lifecycle
graph TD
A[Current Branch: main] --> B[Save Working Changes]
B --> C[Switch to Feature Branch]
C --> D[Reinstall Dependencies]
D --> E[Rebuild Test Artifacts]
E --> F[Verify Test Configuration]
F --> G[Execute Tests]
G --> H[Switch Back to main]
H --> B
Critical Components of Safe Context Switching
- Working Directory State Management: Ensuring no uncommitted changes cause conflicts
- Dependency Synchronization: Updating packages when
package.json
or requirements differ - Build Artifact Management: Clearing cached or compiled test files
- Environment Variable Isolation: Preventing configuration leakage between branches
- Database State Reset: Ensuring test databases reflect branch-specific schemas
Practical Implementation
1. Pre-Switch: Checking and Saving Current State
Before switching branches, always verify your working directory status:
# Check current branch and uncommitted changes
$ git status
On branch main
Changes not staged for commit:
modified: tests/login.spec.js
modified: package.json
Untracked files:
tests/new-feature.spec.js
Safe stashing approach:
# Stash changes with descriptive message
$ git stash push -m "WIP: login test refactoring"
# Verify clean state
$ git status
On branch main
nothing to commit, working tree clean
2. Switching Branches Safely
# Switch to existing branch
$ git checkout feature/payment-integration
# Or create and switch to new branch
$ git checkout -b feature/new-test-suite
# Verify switch was successful
$ git branch
main
* feature/payment-integration
feature/user-authentication
3. Post-Switch: Dependency Synchronization
After switching, dependencies may have changed. Create an automated script:
// scripts/post-branch-switch.js
const { execSync } = require('child_process');
const fs = require('fs');
const crypto = require('crypto');
class BranchSwitchManager {
constructor() {
this.lockfilePath = 'package-lock.json';
this.cachePath = '.branch-cache/';
}
// Calculate hash of dependency files
getDependencyHash() {
const packageJson = fs.readFileSync('package.json', 'utf8');
return crypto.createHash('md5').update(packageJson).digest('hex');
}
// Check if dependencies need reinstallation
needsDependencyUpdate() {
const currentHash = this.getDependencyHash();
const cacheFile = `${this.cachePath}${this.getCurrentBranch()}.hash`;
if (!fs.existsSync(cacheFile)) return true;
const cachedHash = fs.readFileSync(cacheFile, 'utf8');
return currentHash !== cachedHash;
}
getCurrentBranch() {
return execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
}
// Update dependencies and cache
updateDependencies() {
console.log('๐ Detecting dependency changes...');
if (this.needsDependencyUpdate()) {
console.log('๐ฆ Installing dependencies...');
execSync('npm ci', { stdio: 'inherit' });
// Cache the hash
if (!fs.existsSync(this.cachePath)) {
fs.mkdirSync(this.cachePath, { recursive: true });
}
const hash = this.getDependencyHash();
fs.writeFileSync(
`${this.cachePath}${this.getCurrentBranch()}.hash`,
hash
);
console.log('โ
Dependencies synchronized');
} else {
console.log('โ
Dependencies already up to date');
}
}
// Clean test artifacts
cleanArtifacts() {
console.log('๐งน Cleaning test artifacts...');
const dirsToClean = ['test-results', 'screenshots', 'videos', '.nyc_output'];
dirsToClean.forEach(dir => {
if (fs.existsSync(dir)) {
fs.rmSync(dir, { recursive: true, force: true });
console.log(` Removed: ${dir}`);
}
});
}
// Execute full switch routine
execute() {
console.log(`\n๐ Post-switch routine for branch: ${this.getCurrentBranch()}\n`);
this.updateDependencies();
this.cleanArtifacts();
console.log('\nโจ Branch context ready for testing!\n');
}
}
// Execute if run directly
if (require.main === module) {
const manager = new BranchSwitchManager();
manager.execute();
}
module.exports = BranchSwitchManager;
Usage:
# After switching branches, run:
$ node scripts/post-branch-switch.js
๐ Post-switch routine for branch: feature/payment-integration
๐ Detecting dependency changes...
๐ฆ Installing dependencies...
โ
Dependencies synchronized
๐งน Cleaning test artifacts...
Removed: test-results
Removed: screenshots
โจ Branch context ready for testing!
4. Configuration Management Across Branches
Create branch-specific configuration with automatic loading:
// config/branch-config.js
const fs = require('fs');
const path = require('path');
class BranchConfig {
constructor() {
this.branch = this.getCurrentBranch();
this.baseConfig = this.loadConfig('config/base.config.js');
this.branchConfig = this.loadBranchSpecificConfig();
}
getCurrentBranch() {
const { execSync } = require('child_process');
return execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
}
loadConfig(configPath) {
if (fs.existsSync(configPath)) {
return require(path.resolve(configPath));
}
return {};
}
loadBranchSpecificConfig() {
// Try branch-specific config first
const branchConfigPath = `config/branches/${this.branch}.config.js`;
if (fs.existsSync(branchConfigPath)) {
console.log(`๐ Loading config for branch: ${this.branch}`);
return require(path.resolve(branchConfigPath));
}
// Fall back to default
console.log('๐ Using default configuration');
return {};
}
getConfig() {
// Merge configs with branch-specific overriding base
return {
...this.baseConfig,
...this.branchConfig,
metadata: {
branch: this.branch,
loadedAt: new Date().toISOString()
}
};
}
}
module.exports = new BranchConfig().getConfig();
Example branch-specific config:
// config/branches/feature-payment-integration.config.js
module.exports = {
baseURL: 'https://practiceautomatedtesting.com',
testTimeout: 30000,
retries: 2,
// Feature-specific settings
features: {
paymentGateway: true,
mockPayments: true
},
// Test data specific to this branch
testData: {
testCardNumber: '4111111111111111',
testCVV: '123'
}
};
5. Automated Branch Switch Workflow
Create a complete Git hook to automate the entire process:
# .git/hooks/post-checkout
#!/bin/bash
# Get branch names
PREV_BRANCH=$(git reflog -1 | grep -oP 'from \K[^ ]+')
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
# Only run if actually switching branches (not just checking out files)
if [ $3 == 1 ]; then
echo "๐ Switched from '$PREV_BRANCH' to '$CURRENT_BRANCH'"
# Run dependency check and artifact cleanup
if [ -f "scripts/post-branch-switch.js" ]; then
node scripts/post-branch-switch.js
fi
# Display test suite summary for new branch
echo "\n๐ Test Suite Summary for '$CURRENT_BRANCH':"
find tests -name "*.spec.js" | wc -l | xargs echo " Total test files:"
fi
Make it executable:
$ chmod +x .git/hooks/post-checkout
6. Playwright-Specific Context Switching
For Playwright tests, manage browser contexts per branch:
// tests/helpers/branch-aware-setup.js
const { chromium } = require('@playwright/test');
const branchConfig = require('../../config/branch-config');
class BranchAwareTestSetup {
async setupBrowser() {
const browser = await chromium.launch({
headless: branchConfig.headless || true
});
// Create context with branch-specific settings
const context = await browser.newContext({
baseURL: branchConfig.baseURL,
viewport: branchConfig.viewport || { width: 1280, height: 720 },
storageState: this.getStorageStatePath(),
recordVideo: branchConfig.recordVideo ? {
dir: `videos/${branchConfig.metadata.branch}/`
} : undefined
});
return { browser, context };
}
getStorageStatePath() {
const branch = branchConfig.metadata.branch;
const statePath = `storage-states/${branch}-auth.json`;
return require('fs').existsSync(statePath) ? statePath : undefined;
}
async setupTestData(page) {
// Load branch-specific test data
const testData = branchConfig.testData || {};
// Inject as window variable for tests to access
await page.addInitScript((data) => {
window.BRANCH_TEST_DATA = data;
}, testData);
}
}
module.exports = BranchAwareTestSetup;
Using in tests:
// tests/checkout.spec.js
const { test, expect } = require('@playwright/test');
const BranchAwareTestSetup = require('./helpers/branch-aware-setup');
test.describe('Checkout Flow', () => {
let setup;
test.beforeAll(async () => {
setup = new BranchAwareTestSetup();
});
test('should process payment with branch-specific configuration', async () => {
const { browser, context } = await setup.setupBrowser();
const page = await context.newPage();
await setup.setupTestData(page);
// Navigate to test site
await page.goto('/shop');
// Access branch-specific test data
const testCardNumber = await page.evaluate(() => {
return window.BRANCH_TEST_DATA.testCardNumber;
});
// Add product and proceed to checkout
await page.click('text=Add to Cart');
await page.click('text=Checkout');
// Use branch-specific test data
await page.fill('#card-number', testCardNumber);
await page.click('text=Complete Purchase');
await expect(page.locator('.success-message')).toBeVisible();
await browser.close();
});
});
Common Mistakes and Debugging
Mistake 1: Not Cleaning Build Artifacts
Problem: Old compiled files persist after branch switch, causing test failures.
# โ BAD: Switching without cleanup
$ git checkout feature/new-api
$ npm test # Uses old artifacts!
# โ
GOOD: Clean before testing
$ git checkout feature/new-api
$ rm -rf node_modules/.cache test-results
$ npm test
Mistake 2: Dependency Cache Conflicts
Problem: npm/yarn caches cause version conflicts.
# Debug dependency issues
$ npm ls playwright
$ npm ls --depth=0 # Check top-level dependencies
# Nuclear option: complete clean install
$ rm -rf node_modules package-lock.json
$ npm install
Mistake 3: Uncommitted Changes Causing Conflicts
Problem: Branch switch fails due to uncommitted work.
# If git checkout fails:
$ git status
On branch main
Changes not staged for commit:
modified: tests/login.spec.js
# Solution 1: Stash changes
$ git stash push -m "WIP: updating login tests"
# Solution 2: Commit to temporary branch
$ git checkout -b temp/save-work
$ git add .
$ git commit -m "WIP: save point"
$ git checkout main
Mistake 4: Environment Variables Not Updated
Problem: Tests use wrong environment configuration after switch.
// โ
GOOD: Always reload environment per test run
// playwright.config.js
require('dotenv').config({
path: `.env.${process.env.BRANCH || 'default'}`
});
module.exports = {
use: {
baseURL: process.env.BASE_URL,
// Load fresh config each run
}
};
Debugging Checklist
When tests fail after branch switch:
# 1. Verify correct branch
$ git branch
$ git status
# 2. Check dependency versions
$ npm ls playwright
$ npm ls @playwright/test
# 3. Clear all caches
$ rm -rf node_modules/.cache
$ npx playwright cache clear
# 4. Verify configuration loaded correctly
$ node -e "console.log(require('./config/branch-config'))"
# 5. Run single test in debug mode
$ npx playwright test tests/login.spec.js --debug
# 6. Check for conflicting processes
$ lsof -i :3000 # Check if dev server running
Pro Tip: Create a diagnostic script:
#!/bin/bash
# scripts/diagnose.sh
echo "๐ Test Environment Diagnostics"
echo "================================"
echo "Branch: $(git branch --show-current)"
echo "Node: $(node --version)"
echo "Playwright: $(npx playwright --version)"
echo "\nDependency Hash: $(md5sum package.json | cut -d' ' -f1)"
echo "\nTest Files: $(find tests -name '*.spec.js' | wc -l)"
echo "\nUncommitted Changes: $(git status --short | wc -l)"
By implementing these patterns, you’ll maintain clean, isolated test contexts across branches, preventing the common pitfalls that plague CI/CD pipelines in multi-branch development workflows.
Hands-On Practice
EXERCISE
Hands-On Exercise: Multi-Branch Test Automation Pipeline
Scenario
You’re managing a test automation framework for an e-commerce application with multiple active branches: main
, feature/payment-gateway
, feature/ui-redesign
, and hotfix/cart-bug
. Your task is to create a robust branch-switching automation workflow that handles test execution across these branches while managing dependencies, test data, and parallel execution.
Task
Build a branch context manager that:
- Switches between branches safely
- Manages branch-specific test configurations
- Handles test data isolation per branch
- Executes tests in parallel across branches
- Generates consolidated reports
Step-by-Step Instructions
Step 1: Set Up Branch Manager Class
Create a BranchContextManager
that handles git operations and state management.
Step 2: Implement Configuration Management Build a system to load and apply branch-specific test configurations (database URLs, API endpoints, feature flags).
Step 3: Create Test Data Isolation Implement a mechanism to maintain separate test data sets for each branch.
Step 4: Build Parallel Execution Logic Design a system to run tests on multiple branches simultaneously using separate working directories.
Step 5: Implement Safety Mechanisms Add stash handling, conflict detection, and rollback capabilities.
Step 6: Create Reporting System Generate a consolidated report comparing test results across all branches.
Starter Code
import git
import json
import subprocess
from pathlib import Path
from typing import Dict, List
import concurrent.futures
import shutil
class BranchContextManager:
def __init__(self, repo_path: str, base_test_dir: str):
self.repo_path = Path(repo_path)
self.base_test_dir = Path(base_test_dir)
self.repo = git.Repo(repo_path)
self.original_branch = self.repo.active_branch.name
self.stash_applied = False
def __enter__(self):
"""Context manager entry - stash changes"""
# TODO: Implement stash logic
pass
def __exit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit - restore original state"""
# TODO: Implement cleanup and restore logic
pass
def switch_branch(self, branch_name: str) -> bool:
"""Safely switch to target branch"""
# TODO: Implement safe branch switching
pass
def load_branch_config(self, branch_name: str) -> Dict:
"""Load branch-specific test configuration"""
# TODO: Implement configuration loading
pass
def prepare_test_environment(self, branch_name: str) -> Path:
"""Create isolated test environment for branch"""
# TODO: Implement environment setup
pass
class ParallelBranchTestRunner:
def __init__(self, repo_path: str, branches: List[str]):
self.repo_path = repo_path
self.branches = branches
self.results = {}
def run_tests_on_branch(self, branch_name: str) -> Dict:
"""Execute tests on specific branch"""
# TODO: Implement test execution logic
pass
def run_parallel(self, max_workers: int = 4) -> Dict:
"""Run tests on all branches in parallel"""
# TODO: Implement parallel execution
pass
def generate_comparison_report(self) -> str:
"""Create consolidated report across branches"""
# TODO: Implement report generation
pass
# Your implementation here
def main():
branches = ['main', 'feature/payment-gateway',
'feature/ui-redesign', 'hotfix/cart-bug']
# TODO: Implement complete workflow
pass
if __name__ == "__main__":
main()
Expected Outcome
Your solution should:
- โ Successfully switch between branches without losing uncommitted changes
- โ Load and apply correct configurations for each branch
- โ Execute tests in parallel on 4 branches simultaneously
- โ Maintain isolated test data for each branch
- โ
Generate a comparison report showing:
- Pass/fail status per branch
- Execution time per branch
- Branch-specific failures
- Configuration differences
- โ Return to original branch state after completion
- โ Handle errors gracefully with proper rollback
Solution Approach
- Branch Safety: Use git stash before switching, check for conflicts, maintain branch state history
- Configuration Management: Store branch configs in
config/{branch_name}.json
, validate before loading - Isolation: Create separate working directories using
worktree
or copy repository - Parallel Execution: Use
concurrent.futures.ThreadPoolExecutor
with separate BranchContextManager instances - Error Handling: Implement try-finally blocks, maintain rollback stack, log all operations
- Reporting: Collect results in thread-safe dictionary, format as HTML/JSON with comparison matrix
CONCLUSION
Key Takeaways
โ Branch Context Management is Critical - Proper handling of branch switching with stashing, conflict detection, and state restoration prevents data loss and ensures reliable test execution across multiple development streams.
โ Configuration Isolation Prevents Test Pollution - Branch-specific configurations, test data, and environment variables must be isolated to avoid cross-contamination and ensure accurate test results that reflect each branch’s actual behavior.
โ Parallel Execution Requires Careful Orchestration - Running tests on multiple branches simultaneously demands separate working directories (worktrees), thread-safe operations, and proper resource management to avoid conflicts and maximize efficiency.
โ Safety Mechanisms Are Non-Negotiable - Always implement rollback capabilities, error handling, and state verification. A failed branch switch should never leave the repository in an inconsistent state or lose uncommitted work.
โ Consolidated Reporting Drives Decision Making - Comparing test results across branches helps identify branch-specific regressions, configuration issues, and integration problems before merging, significantly reducing production bugs.
When to Apply These Techniques
- Managing test automation for teams with multiple active feature branches
- Implementing CI/CD pipelines that test multiple branches simultaneously
- Performing pre-merge validation across different development streams
- Debugging branch-specific test failures without affecting main development
- Running regression suites against multiple release candidates
- Validating hotfixes against both production and development branches
Next Steps
Practice These Skills
- Add branch-specific environment variable management
- Implement automatic dependency installation per branch
- Create branch comparison visualizations (dashboards)
- Add support for remote branch testing
- Implement caching strategies for faster context switching
Related Topics to Explore
- Git Worktree Mastery - Advanced techniques for managing multiple working directories
- Container-Based Test Isolation - Using Docker for complete environment separation
- CI/CD Pipeline Optimization - Integrating branch-based testing into GitLab/GitHub Actions
- Test Data Versioning - Managing test datasets across branches and environments
- Distributed Test Execution - Scaling branch testing across multiple machines
- Feature Flag Testing - Testing different configurations within the same branch