Module 5: Rebasing: Creating Clean Test History
Master rebasing to maintain a linear, clean history in your test automation projects. Learn when rebasing is preferable to merging, how to rebase feature branches onto updated main branches, and practice interactive rebasing to clean up test commits before sharing with the team.
Understanding Rebase Fundamentals with Test Automation Commits
Why This Matters
As a test automation engineer, you’ve likely encountered repositories with tangled commit histories—merge commits everywhere, work-in-progress commits cluttering the log, and difficulty tracking when specific test cases were added or modified. When reviewing a pull request with 15 commits like “fix typo,” “actually fix it,” and “WIP - trying something,” understanding what changed becomes a frustrating archaeological dig.
Rebasing solves a critical problem: it allows you to maintain a clean, linear project history that tells a clear story of how your test suite evolved. Instead of merge commits creating a web of parallel timelines every time someone pulls the latest changes, rebasing creates a straight line of commits that’s easy to read, review, and debug.
When You’ll Use This Skill
You’ll use rebasing in your daily test automation work when:
- Before creating pull requests: Clean up your experimental commits, squash related changes, and present a polished set of commits that reviewers can easily understand
- Syncing feature branches: Keep your test automation feature branch up-to-date with the main branch without creating merge commit noise
- Maintaining test suites: When your teammate adds new framework utilities to main while you’re working on new test cases, rebase to integrate their changes seamlessly
- Collaborative debugging: When multiple team members are adding tests for the same feature, rebasing helps avoid merge commit clutter
Common Pain Points Addressed
This lesson tackles real problems test automation teams face:
- Messy commit histories that make it hard to understand when and why a test was added or changed
- Difficulty in code reviews when commits are disorganized or overly granular
- Merge commit pollution that obscures the actual work done in feature branches
- Challenges tracking down bugs when commit history doesn’t tell a coherent story
- Team friction caused by different approaches to integrating changes
Learning Objectives Overview
By the end of this lesson, you’ll have hands-on experience with Git rebase in the context of test automation projects. Here’s how we’ll build your skills:
Understanding When to Rebase vs Merge
We’ll start by examining real scenarios from test automation workflows—adding new test cases, updating test data, refactoring page objects. You’ll learn the decision criteria: when does a clean linear history matter most? When should you preserve branch history with a merge? We’ll cover the golden rule of rebasing and why it matters for team collaboration.
Rebasing Feature Branches
You’ll practice the most common rebasing scenario: your colleague merged new helper functions into main while you were writing tests. We’ll walk through step-by-step how to rebase your feature branch onto the updated main branch, maintaining a clean history while incorporating their changes. You’ll see exactly what happens to your commits during this process.
Interactive Rebasing for Commit Cleanup
This is where rebasing becomes powerful. You’ll learn to use interactive rebase to transform a messy development history (with commits like “added login test,” “fixed selector,” “fixed it again,” “removed debug code”) into clean, logical commits ready for team review. You’ll practice squashing, reordering, rewording, and even dropping commits.
Avoiding Common Pitfalls
We’ll cover critical scenarios where rebasing can cause problems—shared branches, public history, and collaborative work. You’ll learn to recognize warning signs and choose the right approach for different situations.
Handling Rebase Conflicts
Conflicts during rebase are inevitable. You’ll practice resolving them step-by-step in test automation contexts (conflicting test files, competing framework changes) and learn how to abort and restart if things go wrong.
Each objective includes hands-on exercises with a sample test automation repository, so you’ll build muscle memory and confidence with these essential commands.
Core Content
Core Content: Understanding Rebase Fundamentals with Test Automation Commits
Core Concepts Explained
What is Git Rebase?
Git rebase is a powerful version control operation that allows you to integrate changes from one branch into another by moving or “replaying” commits onto a new base commit. Unlike merge, which creates a new commit joining two branches, rebase rewrites the commit history to create a linear progression.
Why Rebase Matters for Test Automation:
- Keeps test automation commit history clean and readable
- Makes it easier to identify when specific tests were added or modified
- Simplifies code reviews by presenting changes in logical order
- Helps maintain CI/CD pipeline clarity
Rebase vs. Merge
graph LR
A[main: C1] --> B[main: C2]
B --> C[feature: C3]
C --> D[feature: C4]
style C fill:#e1f5ff
style D fill:#e1f5ff
With Merge:
# Creates a merge commit
main: C1 - C2 ----------- M5
\ /
feature: C3 - C4
With Rebase:
# Linear history
main: C1 - C2 - C3' - C4'
Interactive Rebase: The Test Automator’s Tool
Interactive rebase (git rebase -i
) allows you to:
- Reorder commits - Organize test additions logically
- Squash commits - Combine multiple test fixes into one
- Edit commit messages - Clarify test purpose
- Drop commits - Remove experimental test code
Practical Code Examples
Setting Up Your Test Automation Repository
First, let’s create a test automation project structure:
# Initialize a new repository
mkdir automation-rebase-demo
cd automation-rebase-demo
git init
# Create test automation structure
mkdir -p tests/ui tests/api
touch tests/ui/login.test.js
touch tests/api/user.test.js
touch package.json
Example 1: Basic Rebase for Test Updates
Create initial commits representing test development:
# Initial commit with basic test
echo '{
"name": "automation-tests",
"scripts": {
"test": "jest"
}
}' > package.json
git add package.json
git commit -m "Initialize test project"
# Add login test
cat > tests/ui/login.test.js << 'EOF'
// tests/ui/login.test.js
const { chromium } = require('playwright');
describe('Login Tests', () => {
let browser, page;
beforeEach(async () => {
browser = await chromium.launch();
page = await browser.newPage();
await page.goto('https://practiceautomatedtesting.com/login');
});
afterEach(async () => {
await browser.close();
});
test('should display login form', async () => {
const emailInput = await page.locator('#email');
expect(await emailInput.isVisible()).toBe(true);
});
});
EOF
git add tests/ui/login.test.js
git commit -m "Add basic login test"
# Create a feature branch for new tests
git checkout -b feature/enhanced-tests
# Add more test cases
cat >> tests/ui/login.test.js << 'EOF'
test('should show validation error for empty email', async () => {
await page.click('button[type="submit"]');
const error = await page.locator('.error-message');
expect(await error.textContent()).toContain('Email is required');
});
test('should login with valid credentials', async () => {
await page.fill('#email', 'test@example.com');
await page.fill('#password', 'SecurePass123');
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard');
expect(page.url()).toContain('/dashboard');
});
EOF
git add tests/ui/login.test.js
git commit -m "Add validation and success tests"
Example 2: Rebasing Feature Branch onto Updated Main
# Simulate main branch updates
git checkout main
# Add API tests on main
cat > tests/api/user.test.js << 'EOF'
// tests/api/user.test.js
const axios = require('axios');
describe('User API Tests', () => {
const BASE_URL = 'https://practiceautomatedtesting.com/api';
test('should fetch user profile', async () => {
const response = await axios.get(`${BASE_URL}/users/1`);
expect(response.status).toBe(200);
expect(response.data).toHaveProperty('email');
});
});
EOF
git add tests/api/user.test.js
git commit -m "Add user API tests"
# Now rebase feature branch
git checkout feature/enhanced-tests
git rebase main
Terminal Output:
$ git rebase main
Successfully rebased and updated refs/heads/feature/enhanced-tests.
Example 3: Interactive Rebase to Clean Up Test Commits
Let’s create a messy commit history and clean it up:
# Create multiple small commits (typical during test development)
echo "// WIP test" >> tests/ui/login.test.js
git add tests/ui/login.test.js
git commit -m "WIP: trying something"
echo "// Fixed typo" >> tests/ui/login.test.js
git add tests/ui/login.test.js
git commit -m "fix typo"
echo "// Added assertion" >> tests/ui/login.test.js
git add tests/ui/login.test.js
git commit -m "add assertion to test"
# View commit history
git log --oneline -5
Terminal Output:
$ git log --oneline -5
a1b2c3d add assertion to test
d4e5f6g fix typo
h7i8j9k WIP: trying something
m1n2o3p Add validation and success tests
q4r5s6t Add user API tests
Now clean it up with interactive rebase:
# Start interactive rebase for last 3 commits
git rebase -i HEAD~3
Interactive Rebase Editor:
pick h7i8j9k WIP: trying something
pick d4e5f6g fix typo
pick a1b2c3d add assertion to test
# Rebase m1n2o3p..a1b2c3d onto m1n2o3p (3 commands)
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
Change to:
pick h7i8j9k WIP: trying something
fixup d4e5f6g fix typo
fixup a1b2c3d add assertion to test
Then reword the commit message:
Enhance login tests with additional scenarios
- Add timeout test case
- Improve assertion specificity
- Update selectors for better stability
Result:
$ git log --oneline -3
z9y8x7w Enhance login tests with additional scenarios
m1n2o3p Add validation and success tests
q4r5s6t Add user API tests
Example 4: Reordering Test Commits Logically
# Create commits in random order
git checkout -b feature/test-suite
# Add API test first
cat > tests/api/products.test.js << 'EOF'
describe('Product API', () => {
test('should list products', async () => {
// Test implementation
});
});
EOF
git add tests/api/products.test.js
git commit -m "Add product API tests"
# Add config file (should be first)
cat > tests/config.js << 'EOF'
module.exports = {
baseUrl: 'https://practiceautomatedtesting.com',
timeout: 30000
};
EOF
git add tests/config.js
git commit -m "Add test configuration"
# Add helper utilities (should be second)
cat > tests/helpers.js << 'EOF'
module.exports = {
waitForElement: (selector) => {
// Helper implementation
}
};
EOF
git add tests/helpers.js
git commit -m "Add test helpers"
# Reorder with interactive rebase
git rebase -i HEAD~3
Reorder commits to:
pick <hash> Add test configuration
pick <hash> Add test helpers
pick <hash> Add product API tests
Example 5: Handling Rebase Conflicts in Test Files
# Simulate conflict scenario
git checkout main
# Update login test on main
sed -i '10s/.*/ test("should validate email format", async () => {/' tests/ui/login.test.js
git add tests/ui/login.test.js
git commit -m "Update login test validation"
# Try to rebase feature branch
git checkout feature/enhanced-tests
git rebase main
Conflict Output:
$ git rebase main
Auto-merging tests/ui/login.test.js
CONFLICT (content): Merge conflict in tests/ui/login.test.js
error: could not apply a1b2c3d... Add validation and success tests
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
Resolve the conflict:
// tests/ui/login.test.js
<<<<<<< HEAD
test('should validate email format', async () => {
await page.fill('#email', 'invalid-email');
=======
test('should show validation error for empty email', async () => {
await page.click('button[type="submit"]');
>>>>>>> a1b2c3d... Add validation and success tests
Resolution strategy:
// Keep both tests - they test different scenarios
test('should validate email format', async () => {
await page.fill('#email', 'invalid-email');
await page.click('button[type="submit"]');
const error = await page.locator('.error-message');
expect(await error.textContent()).toContain('Invalid email format');
});
test('should show validation error for empty email', async () => {
await page.click('button[type="submit"]');
const error = await page.locator('.error-message');
expect(await error.textContent()).toContain('Email is required');
});
Continue rebase:
git add tests/ui/login.test.js
git rebase --continue
Common Mistakes Section
Mistake 1: Rebasing Public/Shared Branches
❌ Don’t do this:
git checkout main
git rebase feature/tests # Never rebase main if others are using it!
✅ Instead:
git checkout feature/tests
git rebase main # Always rebase your feature branch onto main
Why: Rebasing rewrites commit history. If others have pulled the branch, their history becomes incompatible.
Mistake 2: Losing Work During Interactive Rebase
❌ Common error:
# In interactive rebase editor
drop a1b2c3d Important test cases # Accidentally dropping needed commits
✅ Create a backup first:
git branch backup-feature-tests
git rebase -i HEAD~5
# If something goes wrong:
git rebase --abort
git reset --hard backup-feature-tests
Mistake 3: Not Testing After Rebase
✅ Always verify tests still pass:
git rebase main
npm test # Run your test suite!
# Only push if tests pass
git push origin feature/tests --force-with-lease
Mistake 4: Using --force
Instead of --force-with-lease
❌ Dangerous:
git push --force # Can overwrite others' work
✅ Safer:
git push --force-with-lease # Fails if remote has changes you don't have
Debugging Rebase Issues
Check rebase status:
git status # Shows current rebase state
git log --oneline --graph --all # Visualize branch history
Abort if things go wrong:
git rebase --abort # Returns to pre-rebase state
Skip problematic commits:
git rebase --skip # Use cautiously - skips current commit entirely
View what’s being applied:
cat .git/rebase-merge/patch # See current patch being applied
Best Practices Summary
- Always rebase feature branches, never shared branches
- Create backup branches before complex rebases
- Run tests after rebasing
- Use
--force-with-lease
for pushing rebased branches - Squash WIP and fix commits before merging
- Write clear commit messages that explain test purpose
- Keep commits atomic - one logical change per commit
Hands-On Practice
EXERCISE AND CONCLUSION
🏋️ Hands-On Exercise
Task: Refactor Test Suite History Using Rebase
You’re working on a test automation project where your commit history has become messy. Your task is to clean up the history of a feature branch containing Selenium tests before merging to main.
Scenario Setup
You have a feature branch feature/login-tests
with the following commits:
- “Add login test file”
- “Fix typo”
- “Add valid credentials test”
- “WIP - debugging”
- “Add invalid credentials test”
- “Update comments”
- “Add forgot password test”
Meanwhile, the main
branch has been updated with new dependencies and configuration changes.
Step-by-Step Instructions
Part 1: Interactive Rebase to Clean History
- Create the messy branch:
git checkout -b feature/login-tests
# Make the 7 commits as listed above (simulate messy history)
- Start interactive rebase:
git rebase -i HEAD~7
-
Clean up the commits:
- Squash “Fix typo” into “Add login test file”
- Drop “WIP - debugging”
- Squash “Update comments” into the relevant test commits
- Keep the three main test commits as separate, meaningful commits
-
Your final history should have 3 clean commits:
- “Add login page test structure and valid credentials test”
- “Add invalid credentials test case”
- “Add forgot password test scenario”
Part 2: Rebase onto Updated Main
- Simulate main branch updates:
git checkout main
# Add a commit updating package.json or test configuration
git commit -m "Update Selenium WebDriver to v4.15"
- Rebase your feature branch:
git checkout feature/login-tests
git rebase main
- Resolve any conflicts that arise in:
- Dependencies
- Configuration files
- Import statements
Part 3: Force Push Safely
- Update your remote branch:
git push --force-with-lease origin feature/login-tests
Expected Outcome
✅ Success Criteria:
- Feature branch has exactly 3 well-named commits
- All commits build upon the latest main branch
- No “WIP” or “fix typo” commits remain
- Tests run successfully after rebase
- Force push completed without overwriting others’ work
Starter Code
login_tests.py (starting point):
from selenium import webdriver
from selenium.webdriver.common.by import By
import pytest
class TestLogin:
def setup_method(self):
self.driver = webdriver.Chrome()
self.driver.get("https://example.com/login")
def teardown_method(self):
self.driver.quit()
# Add your test methods through multiple commits
# to simulate the messy commit history
Solution Approach
Interactive Rebase Commands:
# In the rebase interactive editor, change:
pick abc1234 Add login test file
squash def5678 Fix typo # Change 'pick' to 'squash'
pick ghi9012 Add valid credentials test
drop jkl3456 WIP - debugging # Change 'pick' to 'drop'
pick mno7890 Add invalid credentials test
squash pqr2345 Update comments # Squash into previous
pick stu6789 Add forgot password test
Conflict Resolution Pattern:
# When conflicts occur:
1. git status # Identify conflicted files
2. # Edit files to resolve conflicts
3. git add <resolved-files>
4. git rebase --continue
5. # Repeat until rebase completes
🎓 Key Takeaways
-
Interactive rebase (
git rebase -i
) is a powerful tool for cleaning up commit history before sharing, especially useful for squashing “fix typo” or “WIP” commits in test development -
Rebasing onto updated branches keeps your test automation feature branches current with the latest dependencies and framework changes, reducing integration conflicts
-
Always use
--force-with-lease
instead of--force
when pushing rebased branches to protect against overwriting collaborators’ work -
Clean commit history improves team collaboration by making it easier to understand what tests were added, review changes during PR, and troubleshoot when tests fail
-
Rebase vs. Merge: Use rebase for local/feature branch cleanup and creating linear history; use merge for integrating completed features into shared branches
🚀 Next Steps
Practice These Skills
- Daily Practice: Before submitting PRs, use interactive rebase to clean up your test commits
- Experiment Safely: Create a practice repository and intentionally create conflicts to practice resolution
- Team Workflow: Discuss rebase policies with your team (when to rebase vs. merge)
Related Topics to Explore
- Git Cherry-Pick: Selectively apply specific test commits to other branches
- Git Reflog: Recover from rebase mistakes and restore previous states
- Rebase Strategies: Learn about
git rebase --onto
for advanced branch management - CI/CD Integration: How rebasing affects automated test pipelines and build history
- Merge Conflict Tools: Explore visual merge tools (KDiff3, P4Merge) for complex test file conflicts
- Git Hooks: Automate pre-rebase checks to ensure tests pass before history modification
Additional Resources
- Practice rebasing with pull request workflows in your current test automation project
- Set up git aliases for common rebase commands:
git config --global alias.rb 'rebase -i'
- Review your team’s branching strategy documentation to understand when rebase is appropriate
Pro Tip: Create a backup branch (git branch backup-branch
) before performing complex rebases. This gives you a safety net if something goes wrong!