Module 8: Fixing Mistakes and Resolving Merge Conflicts
Learn to handle the inevitable: mistakes and conflicts in test code. Practice undoing commits, recovering deleted test files, and resolving merge conflicts in test scripts, configuration files, and test data. Build confidence in recovering from common Git problems without losing work.
Resolving Merge Conflicts in Test Automation Code
Why This Matters
Picture this: You’ve spent the morning writing comprehensive API tests for a new feature. Meanwhile, your teammate has been refactoring the same test configuration file to support multiple environments. When you try to merge your branches, Git throws a merge conflict. Your test suite won’t run until you resolve it, and you’re worried about breaking either your new tests or the refactored configuration.
This is one of the most common scenarios in collaborative test automation.
Merge conflicts are inevitable when multiple test engineers work on the same codebase. Unlike production code conflicts, test automation conflicts carry unique risks:
- Silent test failures: A poorly resolved conflict might cause tests to pass incorrectly or skip critical scenarios
- Lost test coverage: Accidentally overwriting test cases means bugs slip through
- Broken CI/CD pipelines: Unresolved conflicts in configuration files halt automated builds
- Corrupted test data: Merging incompatible test datasets can invalidate your entire test suite
When You’ll Use These Skills
You’ll encounter merge conflicts regularly when:
- Multiple engineers modify the same test files during parallel feature development
- Test configurations diverge between different branches (staging vs. production settings)
- Test data updates happen simultaneously across different test scenarios
- Framework upgrades require changes to multiple test files at once
- Refactoring efforts overlap with new test development
Common Pain Points This Lesson Addresses
- Fear of losing work: Not knowing how to safely resolve conflicts without destroying your tests
- Unclear conflict markers: Understanding what
<<<<<<<
,=======
, and>>>>>>>
mean in your test code - Configuration chaos: Merging environment-specific settings without breaking existing setups
- Test data conflicts: Choosing which version of test data to keep when both have valid changes
- Post-merge uncertainty: Not knowing if your tests still work correctly after resolving conflicts
- Repeat conflicts: Dealing with the same conflicts over and over due to poor workflow practices
What You’ll Learn
This lesson takes you from conflict anxiety to conflict confidence. By the end, you’ll handle merge conflicts in test automation code as a routine part of your workflow.
Learning Path Overview
First, you’ll learn to identify and understand merge conflicts specific to test automation repositories. You’ll recognize the anatomy of conflicts in test scripts versus configuration files, and understand why Git couldn’t automatically merge them.
Next, you’ll master resolving conflicts in test script files while preserving critical test logic. You’ll practice techniques for combining test cases from different branches, ensuring no test coverage is lost in the merge.
Then, you’ll tackle conflicts in test configuration files and environment settings—often the trickiest conflicts because they involve values that must remain environment-specific. You’ll learn strategies for merging without breaking existing test environments.
Following that, you’ll handle conflicting test data files, where both versions may contain valid test scenarios. You’ll learn to merge datasets intelligently and maintain referential integrity.
Throughout the lesson, you’ll use Git conflict resolution tools and markers effectively, understanding exactly what each section means and how to leverage IDE tools to simplify the process.
Critically, you’ll learn to validate test suite functionality after resolving conflicts—the step many engineers skip that leads to broken tests reaching the main branch.
Finally, you’ll discover best practices to minimize future merge conflicts, including branching strategies, communication patterns, and file organization techniques that reduce conflict frequency.
By lesson end, you’ll confidently resolve merge conflicts while maintaining test suite integrity, turning a common source of stress into a manageable skill.
Core Content
Core Content: Resolving Merge Conflicts in Test Automation Code
Core Concepts Explained
Understanding Merge Conflicts in Test Automation
Merge conflicts occur when Git cannot automatically reconcile differences between two branches. In test automation projects, conflicts commonly arise in:
- Test files when multiple team members modify the same test cases
- Page Object Models when UI element locators change
- Configuration files when different environments are updated simultaneously
- Test data files when test datasets are modified in parallel
The Anatomy of a Merge Conflict
When a conflict occurs, Git marks the conflicting sections in your files:
<<<<<<< HEAD (Current Change)
describe('Login Test', () => {
it('should login with valid credentials', () => {
cy.visit('https://practiceautomatedtesting.com/login');
cy.get('#username').type('testuser@example.com');
cy.get('#password').type('SecurePass123!');
=======
describe('User Authentication', () => {
it('validates successful login', () => {
cy.visit('https://practiceautomatedtesting.com/login');
cy.get('[data-testid="username"]').type('testuser@example.com');
cy.get('[data-testid="password"]').type('SecurePass123!');
>>>>>>> feature/update-selectors (Incoming Change)
cy.get('#login-button').click();
});
});
Conflict Markers Explained:
<<<<<<< HEAD
- Your current branch changes (what you have)=======
- Divider between changes>>>>>>> branch-name
- Incoming changes from the branch being merged
Conflict Resolution Workflow
graph TD
A[Start Merge/Pull] --> B{Conflicts Detected?}
B -->|No| C[Merge Complete]
B -->|Yes| D[Identify Conflicting Files]
D --> E[Open Conflicted Files]
E --> F[Review Both Changes]
F --> G[Edit to Resolve]
G --> H[Remove Conflict Markers]
H --> I[Test Resolution]
I --> J[Stage Resolved Files]
J --> K[Commit Merge]
K --> C
Step-by-Step Conflict Resolution Process
Step 1: Detect and Identify Conflicts
When you attempt to merge or pull changes:
$ git merge feature/update-tests
Auto-merging tests/login.spec.js
CONFLICT (content): Merge conflict in tests/login.spec.js
Auto-merging pages/loginPage.js
CONFLICT (content): Merge conflict in pages/loginPage.js
Automatic merge failed; fix conflicts and then commit the result.
Check which files have conflicts:
$ git status
On branch main
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: tests/login.spec.js
both modified: pages/loginPage.js
Step 2: Analyze the Conflicts
Example 1: Test Specification Conflict
// File: tests/checkout.spec.js
<<<<<<< HEAD
describe('Checkout Process', () => {
it('should complete purchase with credit card', () => {
cy.visit('https://practiceautomatedtesting.com/shop');
cy.get('.product-item').first().click();
cy.get('#add-to-cart').click();
cy.get('#checkout-btn').click();
// Fill payment details
cy.get('#card-number').type('4111111111111111');
cy.get('#expiry').type('12/25');
cy.get('#cvv').type('123');
cy.get('#submit-payment').click();
cy.get('.success-message').should('be.visible');
});
});
=======
describe('E2E Checkout Flow', () => {
beforeEach(() => {
cy.visit('https://practiceautomatedtesting.com/shop');
});
it('completes purchase successfully', () => {
// Add product to cart
cy.get('[data-testid="product-card"]').first().click();
cy.get('[data-testid="add-to-cart"]').click();
cy.get('[data-testid="cart-checkout"]').click();
// Payment processing
cy.fillPaymentForm({
cardNumber: '4111111111111111',
expiry: '12/25',
cvv: '123'
});
cy.get('[data-testid="confirm-order"]').click();
cy.contains('Order confirmed').should('exist');
});
});
>>>>>>> feature/refactor-checkout-tests
Analysis:
- HEAD version: Uses CSS selectors, inline payment details
- Incoming version: Uses data-testid selectors, custom command for payment, includes beforeEach hook
Step 3: Choose Resolution Strategy
Strategy Options:
- Keep Current (HEAD) - When your changes are correct
- Accept Incoming - When the other branch has better code
- Combine Both - Merge the best of both approaches
- Rewrite - Create a new solution using elements from both
Recommended Resolution (Combining Both):
// File: tests/checkout.spec.js
describe('E2E Checkout Flow', () => {
beforeEach(() => {
cy.visit('https://practiceautomatedtesting.com/shop');
});
it('should complete purchase with credit card', () => {
// Add product to cart - using better data-testid selectors
cy.get('[data-testid="product-card"]').first().click();
cy.get('[data-testid="add-to-cart"]').click();
cy.get('[data-testid="cart-checkout"]').click();
// Fill payment details - using custom command for reusability
cy.fillPaymentForm({
cardNumber: '4111111111111111',
expiry: '12/25',
cvv: '123'
});
cy.get('[data-testid="confirm-order"]').click();
// Clear assertion combining both approaches
cy.get('.success-message').should('be.visible');
cy.contains('Order confirmed').should('exist');
});
});
Step 4: Resolve Page Object Conflicts
Example 2: Page Object Model Conflict
// File: pages/loginPage.js
<<<<<<< HEAD
class LoginPage {
constructor() {
this.usernameInput = '#username';
this.passwordInput = '#password';
this.loginButton = '#login-button';
}
login(username, password) {
cy.get(this.usernameInput).type(username);
cy.get(this.passwordInput).type(password);
cy.get(this.loginButton).click();
}
}
=======
class LoginPage {
get usernameInput() { return cy.get('[data-testid="username"]'); }
get passwordInput() { return cy.get('[data-testid="password"]'); }
get loginButton() { return cy.get('[data-testid="login-btn"]'); }
get errorMessage() { return cy.get('.error-alert'); }
login(username, password) {
this.usernameInput.type(username);
this.passwordInput.type(password);
this.loginButton.click();
}
verifyLoginError(expectedMessage) {
this.errorMessage.should('contain', expectedMessage);
}
}
>>>>>>> feature/improve-page-objects
Best Resolution (Accept incoming with improvements):
// File: pages/loginPage.js
class LoginPage {
// Using getters for dynamic element selection
get usernameInput() { return cy.get('[data-testid="username"]'); }
get passwordInput() { return cy.get('[data-testid="password"]'); }
get loginButton() { return cy.get('[data-testid="login-btn"]'); }
get errorMessage() { return cy.get('.error-alert'); }
get successMessage() { return cy.get('.success-alert'); }
visit() {
cy.visit('https://practiceautomatedtesting.com/login');
return this;
}
login(username, password) {
this.usernameInput.type(username);
this.passwordInput.type(password);
this.loginButton.click();
return this;
}
verifyLoginError(expectedMessage) {
this.errorMessage.should('contain', expectedMessage);
return this;
}
verifyLoginSuccess() {
this.successMessage.should('be.visible');
return this;
}
}
export default new LoginPage();
Step 5: Handle Configuration Conflicts
Example 3: Configuration File Conflict
// File: cypress.config.js
<<<<<<< HEAD
module.exports = {
e2e: {
baseUrl: 'https://practiceautomatedtesting.com',
viewportWidth: 1280,
viewportHeight: 720,
video: true,
screenshotOnRunFailure: true
}
}
=======
module.exports = {
e2e: {
baseUrl: 'https://practiceautomatedtesting.com',
viewportWidth: 1920,
viewportHeight: 1080,
retries: {
runMode: 2,
openMode: 0
},
defaultCommandTimeout: 10000
}
}
>>>>>>> feature/update-config
Resolution (Combine all useful settings):
// File: cypress.config.js
module.exports = {
e2e: {
baseUrl: 'https://practiceautomatedtesting.com',
// Use larger viewport from feature branch
viewportWidth: 1920,
viewportHeight: 1080,
// Keep video settings from HEAD
video: true,
screenshotOnRunFailure: true,
// Add retry logic from feature branch
retries: {
runMode: 2,
openMode: 0
},
// Add timeout from feature branch
defaultCommandTimeout: 10000
}
}
Step 6: Stage and Commit Resolved Files
After resolving all conflicts:
# Run tests to verify resolution
$ npm test
# Stage the resolved files
$ git add tests/checkout.spec.js
$ git add pages/loginPage.js
$ git add cypress.config.js
# Verify all conflicts are resolved
$ git status
On branch main
All conflicts fixed but you are still merging.
(use "git commit" to conclude merge)
Changes to be committed:
modified: tests/checkout.spec.js
modified: pages/loginPage.js
modified: cypress.config.js
# Commit the merge
$ git commit -m "Merge feature branches: resolved conflicts in checkout tests, login page object, and config
- Combined data-testid selectors with existing test structure
- Improved page object with getter pattern and method chaining
- Merged configuration settings from both branches
- All tests passing"
Practical Code Examples
Complete Conflict Resolution Workflow
# Step 1: Attempt merge
$ git checkout main
$ git merge feature/update-selectors
# Step 2: Conflicts detected - check status
$ git status
# Step 3: Open IDE and review conflicts
# (Resolve conflicts in each file as shown above)
# Step 4: Test your resolution
$ npm run test:local
# Output:
# Checkout Flow Tests
# ✓ should complete purchase with credit card (2543ms)
# Login Tests
# ✓ should login with valid credentials (1234ms)
# ✓ should show error for invalid credentials (987ms)
#
# 3 passing (5s)
# Step 5: Stage resolved files
$ git add .
# Step 6: Complete the merge
$ git commit -m "Resolved merge conflicts in test suite"
# Step 7: Push to remote
$ git push origin main
Using Git Tools for Conflict Resolution
Option 1: VS Code Built-in Merge Editor
<!-- SCREENSHOT_NEEDED: IDE
IDE: VS Code
View: 3-way merge editor
Description: Showing Current, Incoming, and Result panels
Placement: in this section -->
Option 2: Command Line Resolution
# Accept all current changes
$ git checkout --ours tests/login.spec.js
# Accept all incoming changes
$ git checkout --theirs pages/loginPage.js
# Then stage the files
$ git add tests/login.spec.js pages/loginPage.js
Option 3: Using Merge Tool
# Configure merge tool (one-time setup)
$ git config --global merge.tool vscode
$ git config --global mergetool.vscode.cmd 'code --wait $MERGED'
# Launch merge tool for conflicts
$ git mergetool
Common Mistakes Section
Mistake 1: Leaving Conflict Markers in Code
Wrong:
describe('Login Test', () => {
<<<<<<< HEAD
it('should login successfully', () => {
=======
it('validates user login', () => {
>>>>>>> feature/update-tests
cy.visit('https://practiceautomatedtesting.com/login');
});
});
Impact: Code won’t run, tests will fail with syntax errors.
Fix: Always remove ALL conflict markers (<<<<<<<
, =======
, >>>>>>>
) before committing.
Mistake 2: Not Testing After Resolution
Wrong Approach:
# Resolve conflict
$ vim tests/checkout.spec.js
$ git add tests/checkout.spec.js
$ git commit -m "Fixed conflict"
$ git push
# Tests fail in CI/CD pipeline!
Correct Approach:
# Resolve conflict
$ vim tests/checkout.spec.js
# TEST FIRST!
$ npm test
# Verify all tests pass
$ git add tests/checkout.spec.js
$ git commit -m "Resolved checkout test conflicts - all tests passing"
$ git push
Mistake 3: Blindly Accepting One Side
Problem: Always choosing “Accept Current” or “Accept Incoming” without analysis.
Example:
// Accepting incoming might lose important test coverage
// Current has: login validation, error handling, success verification
// Incoming has: only login validation
// Don't blindly accept - combine the best parts!
Mistake 4: Forgetting to Stage All Resolved Files
Debugging:
$ git commit
# Error: Committing is not possible because you have unmerged files.
$ git status
Unmerged paths:
both modified: tests/api.spec.js
# Solution: Stage the forgotten file
$ git add tests/api.spec.js
$ git commit -m "Resolved all conflicts"
Mistake 5: Conflicting Test Data
Problem:
// Both branches modify the same test data
// Current: uses email testuser@example.com
// Incoming: uses email newuser@example.com
// Resolution uses: testuser@example.com
// But incoming branch created user with newuser@example.com!
Solution:
// Ensure test data consistency
describe('User Registration', () => {
const testUser = {
email: 'testuser@example.com',
password: 'SecurePass123!'
};
before(() => {
// Clean up and create fresh test user
---
## Hands-On Practice
# EXERCISE
## Hands-On Exercise: Resolving Test Automation Merge Conflicts
### Scenario
You're working on a test automation project with your team. Two developers have been working on the same test file in separate branches. Now it's time to merge, and conflicts have emerged in the test suite.
### Task
Resolve merge conflicts in a Selenium test suite where multiple team members have modified the same test files, then ensure all tests pass successfully.
### Step-by-Step Instructions
#### Setup (Simulating the Conflict)
1. **Clone or create a new repository:**
```bash
mkdir test-merge-practice
cd test-merge-practice
git init
- Create the initial test file on main branch:
Starter Code - test_login.py
:
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
class TestLogin:
def setup_method(self):
self.driver = webdriver.Chrome()
self.driver.get("https://example-app.com/login")
def test_successful_login(self):
self.driver.find_element(By.ID, "username").send_keys("testuser")
self.driver.find_element(By.ID, "password").send_keys("password123")
self.driver.find_element(By.ID, "login-button").click()
assert "Dashboard" in self.driver.title
def teardown_method(self):
self.driver.quit()
- Commit the initial file:
git add test_login.py
git commit -m "Initial login test"
- Create and switch to feature branch A:
git checkout -b feature/add-validation
- Modify
test_login.py
(Developer A’s changes):
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class TestLogin:
def setup_method(self):
self.driver = webdriver.Chrome()
self.driver.implicitly_wait(10)
self.driver.get("https://example-app.com/login")
def test_successful_login(self):
self.driver.find_element(By.ID, "username").send_keys("testuser")
self.driver.find_element(By.ID, "password").send_keys("password123")
self.driver.find_element(By.ID, "login-button").click()
# Added explicit wait
WebDriverWait(self.driver, 10).until(
EC.title_contains("Dashboard")
)
assert "Dashboard" in self.driver.title
def test_invalid_credentials(self):
self.driver.find_element(By.ID, "username").send_keys("wronguser")
self.driver.find_element(By.ID, "password").send_keys("wrongpass")
self.driver.find_element(By.ID, "login-button").click()
error_msg = self.driver.find_element(By.CLASS_NAME, "error-message")
assert "Invalid credentials" in error_msg.text
def teardown_method(self):
self.driver.quit()
- Commit Developer A’s changes:
git add test_login.py
git commit -m "Add validation test and explicit waits"
- Switch back to main and create feature branch B:
git checkout main
git checkout -b feature/add-logout
- Modify
test_login.py
(Developer B’s changes):
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
class TestLogin:
@pytest.fixture(autouse=True)
def setup_method(self):
self.driver = webdriver.Chrome()
self.driver.maximize_window()
self.driver.get("https://example-app.com/login")
yield
self.driver.quit()
def test_successful_login(self):
self.driver.find_element(By.ID, "username").send_keys("testuser")
self.driver.find_element(By.ID, "password").send_keys("password123")
self.driver.find_element(By.ID, "login-button").click()
assert "Dashboard" in self.driver.title
def test_logout_functionality(self):
# Login first
self.driver.find_element(By.ID, "username").send_keys("testuser")
self.driver.find_element(By.ID, "password").send_keys("password123")
self.driver.find_element(By.ID, "login-button").click()
# Then logout
self.driver.find_element(By.ID, "logout-button").click()
assert "Login" in self.driver.title
- Commit Developer B’s changes:
git add test_login.py
git commit -m "Add logout test and use pytest fixtures"
Your Task: Resolve the Conflicts
- Merge feature/add-validation into main:
git checkout main
git merge feature/add-validation
- Now merge feature/add-logout (this will create conflicts):
git merge feature/add-logout
-
Resolve the conflicts by:
- Opening
test_login.py
in your editor - Combining the best elements from both branches:
- Keep pytest fixtures approach (Developer B)
- Keep explicit waits (Developer A)
- Include ALL three test methods
- Remove conflict markers (
<<<<<<<
,=======
,>>>>>>>
)
- Ensuring consistent coding style
- Making sure imports are complete and not duplicated
- Opening
-
After resolving, verify your solution:
git add test_login.py
git commit -m "Merge feature/add-logout with resolved conflicts"
- Run the tests to ensure nothing broke:
pytest test_login.py -v
Expected Outcome
Resolved test_login.py
:
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class TestLogin:
@pytest.fixture(autouse=True)
def setup_method(self):
self.driver = webdriver.Chrome()
self.driver.maximize_window()
self.driver.implicitly_wait(10)
self.driver.get("https://example-app.com/login")
yield
self.driver.quit()
def test_successful_login(self):
self.driver.find_element(By.ID, "username").send_keys("testuser")
self.driver.find_element(By.ID, "password").send_keys("password123")
self.driver.find_element(By.ID, "login-button").click()
WebDriverWait(self.driver, 10).until(
EC.title_contains("Dashboard")
)
assert "Dashboard" in self.driver.title
def test_invalid_credentials(self):
self.driver.find_element(By.ID, "username").send_keys("wronguser")
self.driver.find_element(By.ID, "password").send_keys("wrongpass")
self.driver.find_element(By.ID, "login-button").click()
error_msg = self.driver.find_element(By.CLASS_NAME, "error-message")
assert "Invalid credentials" in error_msg.text
def test_logout_functionality(self):
self.driver.find_element(By.ID, "username").send_keys("testuser")
self.driver.find_element(By.ID, "password").send_keys("password123")
self.driver.find_element(By.ID, "login-button").click()
WebDriverWait(self.driver, 10).until(
EC.title_contains("Dashboard")
)
self.driver.find_element(By.ID, "logout-button").click()
assert "Login" in self.driver.title
Solution Approach
- Identify conflict sections marked by Git
- Analyze both versions to understand what each developer was trying to achieve
- Merge logically by:
- Taking the better structural approach (pytest fixtures)
- Preserving all new test methods from both branches
- Combining improvements (explicit waits + window maximize)
- Ensuring no duplicate imports
- Test thoroughly to verify the merged code works
- Commit with a descriptive message explaining the resolution
KEY TAKEAWAYS
What You’ve Learned
✅ Conflict Resolution Process: Merge conflicts occur when multiple developers modify the same lines of code. Understanding Git conflict markers (<<<<<<<
, =======
, >>>>>>>
) is essential for identifying and resolving these issues.
✅ Evaluating Changes Contextually: Not all conflicts should be resolved by simply “accepting yours” or “accepting theirs.” The best resolution often involves combining improvements from both branches while maintaining code consistency and functionality.
✅ Test-Specific Considerations: When resolving conflicts in test automation code, prioritize preserving test coverage, maintaining consistent wait strategies, and ensuring fixture/setup configurations work for all test methods.
✅ Post-Merge Verification: Always run your test suite after resolving conflicts to ensure the merged code is functional. Merge conflicts can inadvertently break test logic, remove necessary imports, or create syntax errors.
✅ Communication Matters: Many merge conflicts can be prevented or simplified through team communication about who’s working on which files, establishing coding standards, and reviewing changes regularly through pull requests.
When to Apply These Skills
- Daily team collaboration when multiple testers work on the same test suites
- Feature branch merges before deploying test automation updates
- Code reviews to catch potential conflicts early
- CI/CD pipeline failures caused by merge issues
- Test suite refactoring projects involving multiple contributors
NEXT STEPS
What to Practice
- Create intentional conflicts in your own projects to practice resolution without pressure
- Use different merge tools (VS Code, IntelliJ, GitKraken) to find your preferred workflow
- Practice 3-way merges understanding base, theirs, and yours perspectives
- Resolve conflicts in different file types: configuration files (pytest.ini, conftest.py), page objects, and test data files
Related Topics to Explore
- Git Rebase vs Merge: Learn when rebasing is better than merging for cleaner history
- Branch Strategies: Explore GitFlow, trunk-based development, and their impact on conflicts
- Code Review Best Practices: Prevent conflicts through better collaboration workflows
- Continuous Integration: Set up automated testing to catch integration issues early
- Conflict Prevention: Modular test design, page object patterns, and shared utility functions that reduce overlapping work
- Advanced Git Commands:
git mergetool
,git diff
,git log --merge
for better conflict analysis
Recommended Practice Schedule
- Week 1: Practice resolving simple conflicts daily (15 minutes)
- Week 2: Work with a partner creating and resolving realistic conflicts
- Week 3: Apply conflict resolution in your actual team projects
- Ongoing: Review team merge conflicts as learning opportunities
Pro Tip: Keep a personal “conflict resolution cheat sheet” with your team’s specific patterns and decisions for common conflict scenarios. This becomes invaluable for maintaining consistency across your test automation codebase.