Module 2: Understanding Git's Three Areas: Working, Staging, and Repository
Master the fundamental workflow of Git by understanding the three core areas. Learn how test files move through Working Directory, Staging Area, and Repository. Practice selective staging to commit only relevant test changes, and understand how to inspect and manipulate files at each stage.
The Three Areas Explained: Setting Up Your First Test Repository
Why This Matters
The Real-World Problem
Imagine you’ve spent the afternoon updating your test suite. You’ve fixed three failing tests, added two new test cases, updated a configuration file, and created a temporary debug script. Now it’s time to save your work. But here’s the challenge: you don’t want to save everything together.
The debug script is temporary—it shouldn’t be permanently saved. The configuration change is unrelated to your test updates and should be tracked separately. Without understanding how Git manages changes, you’ll end up with messy, confusing version history that mixes unrelated work together.
This is the daily reality for test automation engineers working without a solid grasp of Git’s workflow. You might:
- Accidentally commit temporary test files (like debug outputs or local configuration overrides) that clutter your repository
- Mix unrelated changes in a single commit (fixing a login test AND updating API test data), making it impossible to understand what changed and why
- Lose track of which files have been modified across multiple test suites when working on complex updates
- Struggle to review your own changes before committing, leading to errors slipping into the shared codebase
When You’ll Use This Skill
Understanding Git’s three areas isn’t just theoretical—it’s something you’ll use every single day as a test automation engineer:
- Every time you write or modify test code and need to save your work
- When reviewing changes before commits to ensure you’re not including unwanted files
- When organizing related test updates together (like grouping all login test changes in one commit, separate from checkout test changes)
- During debugging sessions when you create temporary files that should never be committed
- When collaborating with teammates who need clear, organized commits to understand your test changes
Common Pain Points Addressed
This lesson directly solves the confusion that trips up most beginners:
- “Where are my changes?” - Learn to track file states across Git’s three areas
- “Why can’t I commit my changes?” - Understand that files must be staged before committing
- “I committed the wrong files!” - Master selective staging to commit only what you intend
- “What’s the difference between ‘modified’ and ‘staged’?” - Visualize how Git categorizes your changes
- “How do I organize my test updates properly?” - Apply the three-area workflow to real automation scenarios
What You’ll Accomplish
By the end of this lesson, you will:
Practical Skills
- Initialize your first test repository from an empty directory and understand what Git creates behind the scenes
- Create and track test automation files through all three Git areas using the complete workflow
- Use selective staging to choose exactly which test file changes to include in each commit
- Inspect your repository at any time using
git status
to see what’s changed, what’s staged, and what’s committed - Build organized commits that group related test changes together while excluding temporary or unrelated files
Conceptual Understanding
You’ll develop a clear mental model of:
- How Git’s three areas work together as a pipeline for managing changes
- Why the staging area exists and how it gives you precise control over version history
- The lifecycle of a test file from creation through modification to permanent storage
- How to think about organizing test automation work into logical, reviewable chunks
Expected Outcomes
After completing this lesson, you’ll be able to confidently:
- Start version controlling any test automation project with
git init
- Explain to teammates why Git has three areas instead of just “save” and “saved”
- Make deliberate decisions about which changes to commit and when
- Avoid common pitfalls like committing temporary test files or mixing unrelated changes
- Follow professional version control practices used by test automation teams worldwide
This foundational knowledge sets you up for everything that follows in the course—from branching strategies to team collaboration workflows.
Core Content
Core Content: Setting Up Your First Test Repository
1. Core Concepts Explained
Understanding the Three Areas of Test Automation
Every test automation project consists of three fundamental areas that work together:
Area 1: Test Framework Setup
This is your foundation - the tools, libraries, and configuration files that enable test automation. Think of it as building the kitchen before you start cooking.
Key Components:
- Testing Framework: The engine that runs your tests (e.g., pytest, JUnit, NUnit)
- Automation Library: Tools to interact with applications (e.g., Selenium, Playwright)
- Configuration Files: Settings that tell your tools how to behave
- Dependencies: External libraries your project needs
Area 2: Test Code Structure
How you organize your test files and supporting code. Good structure makes tests easy to find, maintain, and scale.
Key Components:
- Test Files: Where your actual test cases live
- Page Objects/Helper Classes: Reusable code for interacting with your application
- Utilities: Common functions used across tests (data generators, wait helpers)
- Test Data: Input values, expected results, test configurations
Area 3: Execution & Reporting
How you run tests and understand results. This brings everything together.
Key Components:
- Test Runners: Commands to execute your tests
- Reports: Visual output showing what passed/failed
- Logs: Detailed information for debugging
- CI/CD Integration: Automated test execution on code changes
2. Practical Code Examples
Step 1: Initialize Your Repository Structure
Create this folder structure for your first automation project:
test-automation-project/
├── tests/
│ ├── __init__.py
│ └── test_login.py
├── pages/
│ ├── __init__.py
│ └── login_page.py
├── utilities/
│ ├── __init__.py
│ └── driver_factory.py
├── config/
│ └── config.py
├── reports/
├── requirements.txt
└── pytest.ini
Step 2: Set Up Dependencies (Area 1)
requirements.txt - Lists all libraries needed:
selenium==4.15.2
pytest==7.4.3
pytest-html==4.1.1
webdriver-manager==4.0.1
pytest.ini - Configures pytest behavior:
[pytest]
# Directory where tests live
testpaths = tests
# Patterns for test discovery
python_files = test_*.py
python_classes = Test*
python_functions = test_*
# Command line options applied by default
addopts =
--html=reports/report.html
--self-contained-html
-v
--tb=short
config/config.py - Centralized settings:
"""
Configuration settings for test automation
"""
class Config:
# Base URL for application under test
BASE_URL = "https://practicesoftwaretesting.com"
# Browser settings
BROWSER = "chrome" # Options: chrome, firefox, edge
HEADLESS = False # Set True to run without browser UI
# Timeout settings (in seconds)
IMPLICIT_WAIT = 10
EXPLICIT_WAIT = 20
# Test user credentials
TEST_USER_EMAIL = "customer@practicesoftwaretesting.com"
TEST_USER_PASSWORD = "welcome01"
Step 3: Create Driver Factory (Area 1)
utilities/driver_factory.py - Manages browser instances:
"""
Factory class to create and configure WebDriver instances
"""
from selenium import webdriver
from selenium.webdriver.chrome.service import Service as ChromeService
from selenium.webdriver.firefox.service import Service as FirefoxService
from webdriver_manager.chrome import ChromeDriverManager
from webdriver_manager.firefox import GeckoDriverManager
from config.config import Config
class DriverFactory:
@staticmethod
def get_driver():
"""
Creates and returns a configured WebDriver instance
based on config settings
"""
browser = Config.BROWSER.lower()
if browser == "chrome":
options = webdriver.ChromeOptions()
if Config.HEADLESS:
options.add_argument("--headless")
# Disable notifications and popups
options.add_argument("--disable-notifications")
options.add_argument("--start-maximized")
driver = webdriver.Chrome(
service=ChromeService(ChromeDriverManager().install()),
options=options
)
elif browser == "firefox":
options = webdriver.FirefoxOptions()
if Config.HEADLESS:
options.add_argument("--headless")
driver = webdriver.Firefox(
service=FirefoxService(GeckoDriverManager().install()),
options=options
)
else:
raise ValueError(f"Unsupported browser: {browser}")
# Apply wait settings
driver.implicitly_wait(Config.IMPLICIT_WAIT)
return driver
Step 4: Create Page Object (Area 2)
pages/login_page.py - Encapsulates login page interactions:
"""
Page Object Model for Login functionality
"""
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from config.config import Config
class LoginPage:
# Locators - Define once, use everywhere
SIGN_IN_MENU = (By.CSS_SELECTOR, "[data-test='nav-sign-in']")
EMAIL_INPUT = (By.ID, "email")
PASSWORD_INPUT = (By.ID, "password")
LOGIN_BUTTON = (By.CSS_SELECTOR, "[data-test='login-submit']")
USER_MENU = (By.CSS_SELECTOR, "[data-test='nav-menu']")
ERROR_MESSAGE = (By.CSS_SELECTOR, "[data-test='login-error']")
def __init__(self, driver):
"""Initialize with WebDriver instance"""
self.driver = driver
self.wait = WebDriverWait(driver, Config.EXPLICIT_WAIT)
def navigate_to_login(self):
"""Navigate to the login page"""
self.driver.get(Config.BASE_URL)
sign_in = self.wait.until(
EC.element_to_be_clickable(self.SIGN_IN_MENU)
)
sign_in.click()
def enter_email(self, email):
"""Enter email into login form"""
email_field = self.wait.until(
EC.visibility_of_element_located(self.EMAIL_INPUT)
)
email_field.clear()
email_field.send_keys(email)
def enter_password(self, password):
"""Enter password into login form"""
password_field = self.driver.find_element(*self.PASSWORD_INPUT)
password_field.clear()
password_field.send_keys(password)
def click_login(self):
"""Click the login button"""
login_btn = self.driver.find_element(*self.LOGIN_BUTTON)
login_btn.click()
def is_logged_in(self):
"""Check if user successfully logged in"""
try:
self.wait.until(
EC.visibility_of_element_located(self.USER_MENU)
)
return True
except:
return False
def get_error_message(self):
"""Retrieve error message text"""
try:
error = self.wait.until(
EC.visibility_of_element_located(self.ERROR_MESSAGE)
)
return error.text
except:
return None
Step 5: Write Your First Test (Area 2)
tests/test_login.py - Actual test cases:
"""
Test cases for login functionality
"""
import pytest
from utilities.driver_factory import DriverFactory
from pages.login_page import LoginPage
from config.config import Config
class TestLogin:
@pytest.fixture(autouse=True)
def setup(self):
"""
Setup runs before each test
Creates driver and page object instances
"""
self.driver = DriverFactory.get_driver()
self.login_page = LoginPage(self.driver)
yield # Test runs here
# Teardown runs after test
self.driver.quit()
def test_successful_login(self):
"""
Test Case: Verify user can login with valid credentials
"""
# Arrange - Navigate to login page
self.login_page.navigate_to_login()
# Act - Perform login
self.login_page.enter_email(Config.TEST_USER_EMAIL)
self.login_page.enter_password(Config.TEST_USER_PASSWORD)
self.login_page.click_login()
# Assert - Verify login success
assert self.login_page.is_logged_in(), "User should be logged in"
def test_login_with_invalid_password(self):
"""
Test Case: Verify error message for invalid password
"""
# Arrange
self.login_page.navigate_to_login()
# Act
self.login_page.enter_email(Config.TEST_USER_EMAIL)
self.login_page.enter_password("WrongPassword123")
self.login_page.click_login()
# Assert
error = self.login_page.get_error_message()
assert error is not None, "Error message should be displayed"
assert "Invalid" in error, f"Unexpected error: {error}"
def test_login_with_empty_fields(self):
"""
Test Case: Verify validation for empty credentials
"""
self.login_page.navigate_to_login()
self.login_page.click_login()
# Should not be logged in
assert not self.login_page.is_logged_in(), \
"Should not login with empty credentials"
Step 6: Execute Tests (Area 3)
Command to run all tests:
# Install dependencies first
pip install -r requirements.txt
# Run all tests
pytest
# Run with detailed output
pytest -v
# Run specific test file
pytest tests/test_login.py
# Run specific test
pytest tests/test_login.py::TestLogin::test_successful_login
3. Common Mistakes Section
Mistake #1: Hardcoding Values in Tests
❌ Wrong:
def test_login(self):
driver.get("https://practicesoftwaretesting.com")
driver.find_element(By.ID, "email").send_keys("test@test.com")
✅ Correct:
def test_login(self):
self.login_page.navigate_to_login()
self.login_page.enter_email(Config.TEST_USER_EMAIL)
Why: Hardcoded values make tests brittle and hard to maintain when URLs or test data change.
Mistake #2: Not Using Waits
❌ Wrong:
driver.find_element(By.ID, "email").send_keys("test@test.com")
# Element might not be loaded yet!
✅ Correct:
email_field = WebDriverWait(driver, 10).until(
EC.visibility_of_element_located((By.ID, "email"))
)
email_field.send_keys("test@test.com")
Debugging Tip: If you see NoSuchElementException
, add explicit waits.
Mistake #3: Not Cleaning Up Resources
❌ Wrong:
def test_something(self):
driver = webdriver.Chrome()
# Test code...
# Driver never closes!
✅ Correct:
@pytest.fixture(autouse=True)
def setup(self):
self.driver = DriverFactory.get_driver()
yield
self.driver.quit() # Always cleanup
Debugging Tip: If Chrome windows stay open after tests, you’re missing driver.quit()
.
Mistake #4: Poor Test Organization
❌ Wrong:
# Everything in one file
def test1(): pass
def test2(): pass
def helper_function(): pass
✅ Correct:
- Tests in
tests/
directory - Page objects in
pages/
directory - Utilities in
utilities/
directory
Debugging Tip: If tests are hard to find or modify, reorganize using the three-area structure.
Mistake #5: Not Using Page Object Model
❌ Wrong:
def test_login(self):
driver.find_element(By.ID, "email").send_keys("test@test.com")
driver.find_element(By.ID, "password").send_keys("pass")
driver.find_element(By.CSS_SELECTOR, "button").click()
✅ Correct:
def test_login(self):
self.login_page.enter_email("test@test.com")
self.login_page.enter_password("pass")
self.login_page.click_login()
Why: Page objects make tests readable and locators reusable. If UI changes, you update one place.
Next Steps: Run your first test and examine the HTML report in reports/report.html
!
Hands-On Practice
Hands-On Exercise
Task: Create Your First Test Repository Structure
Set up a complete test automation repository from scratch with proper organization for tests, test data, and utilities.
Step-by-Step Instructions
Step 1: Initialize Your Repository
# Create a new directory for your test project
mkdir my-first-test-project
cd my-first-test-project
# Initialize git repository
git init
# Create a README file
echo "# My First Test Automation Project" > README.md
Step 2: Create the Three Core Areas
Create the following folder structure:
# Tests area
mkdir -p tests/unit
mkdir -p tests/integration
mkdir -p tests/e2e
# Test Data area
mkdir -p test-data/users
mkdir -p test-data/products
# Utilities area
mkdir -p utils/helpers
mkdir -p utils/config
Step 3: Add Configuration Files
Create a .gitignore
file:
echo "node_modules/
.env
*.log
test-results/
screenshots/" > .gitignore
Create a basic package.json
(for JavaScript/Node.js):
npm init -y
Step 4: Create Your First Test File
Create tests/e2e/login.test.js
:
// Import utilities (we'll create this next)
const { loadTestData } = require('../../utils/helpers/dataLoader');
describe('Login Functionality', () => {
test('should login with valid credentials', () => {
// This is where your test code will go
const userData = loadTestData('users/valid-user.json');
// Test implementation here
});
});
Step 5: Create Test Data File
Create test-data/users/valid-user.json
:
{
"username": "testuser@example.com",
"password": "SecurePass123!",
"expectedName": "Test User"
}
Step 6: Create a Utility Helper
Create utils/helpers/dataLoader.js
:
const fs = require('fs');
const path = require('path');
function loadTestData(relativePath) {
const fullPath = path.join(__dirname, '../../test-data', relativePath);
const rawData = fs.readFileSync(fullPath, 'utf8');
return JSON.parse(rawData);
}
module.exports = { loadTestData };
Step 7: Create Configuration File
Create utils/config/test.config.js
:
module.exports = {
baseUrl: process.env.BASE_URL || 'http://localhost:3000',
timeout: 30000,
browser: 'chromium',
headless: true
};
Step 8: Document Your Structure
Update your README.md
:
# My First Test Automation Project
## Project Structure
my-first-test-project/ ├── tests/ # All test files │ ├── unit/ # Unit tests │ ├── integration/ # Integration tests │ └── e2e/ # End-to-end tests ├── test-data/ # Test data files │ ├── users/ # User-related data │ └── products/ # Product-related data ├── utils/ # Utilities and helpers │ ├── helpers/ # Helper functions │ └── config/ # Configuration files └── README.md
## Running Tests
```bash
npm test
## Expected Outcome
After completing this exercise, you should have:
1. ✅ A properly initialized Git repository
2. ✅ Three distinct areas: tests, test-data, and utils
3. ✅ A sample test file that references test data
4. ✅ A test data JSON file with sample user credentials
5. ✅ A utility function to load test data
6. ✅ A configuration file for test settings
7. ✅ Documentation explaining the structure
## Solution Approach
The key principles applied in this solution:
- **Separation of Concerns**: Each area (tests, data, utils) has a clear responsibility
- **Scalability**: Subdirectories allow growth (unit, integration, e2e)
- **Reusability**: Utilities can be shared across all tests
- **Maintainability**: Clear structure makes it easy to find and update files
- **Version Control**: Proper .gitignore protects sensitive data
---
# Key Takeaways
## What You Learned
- **The Three Core Areas**: Every test automation project should have distinct areas for:
- **Tests**: Organized by type (unit, integration, e2e) or feature
- **Test Data**: Separated from test logic for easy maintenance and reuse
- **Utilities**: Shared helpers, configurations, and support functions
- **Separation Equals Maintainability**: Keeping tests, data, and utilities separate makes your codebase easier to navigate, update, and scale as your project grows.
- **Structure Enables Collaboration**: A well-organized repository allows team members to quickly understand where to find and add new tests, data, or utilities.
- **Configuration Centralization**: Storing configuration in a dedicated location allows easy environment switching without modifying test code.
- **Data-Driven Testing Foundation**: Separating test data enables you to run the same test with different inputs without code changes.
## When to Apply This
- ✓ Starting any new test automation project
- ✓ Refactoring an existing disorganized test suite
- ✓ Onboarding new team members who need clear structure
- ✓ Scaling from a handful of tests to hundreds or thousands
- ✓ Managing multiple test environments (dev, staging, production)
---
# Next Steps
## What to Practice
1. **Expand Your Test Suite**
- Add at least 3 more test files in different categories
- Create corresponding test data files for each test
- Practice importing utilities in each test
2. **Build More Utilities**
- Create a logger utility for consistent test logging
- Build a date/time helper for test data generation
- Write a screenshot utility for test failures
3. **Enhance Your Data Management**
- Create test data for different user roles
- Add invalid data sets for negative testing
- Implement a test data generator utility
4. **Version Control Practice**
- Commit your changes with clear messages
- Create a `.env.example` file for configuration templates
- Practice branching for new test features
## Related Topics to Explore
- **Test Frameworks**: Jest, Mocha, Playwright, Cypress, Selenium
- **Page Object Model**: Advanced organization pattern for UI tests
- **CI/CD Integration**: Running your tests automatically on commit
- **Test Data Management**: Factories, fixtures, and database seeders
- **Environment Configuration**: Managing multiple test environments
- **Reporting**: Integrating test reporters and generating HTML reports
- **Parallel Execution**: Running tests concurrently for faster feedback
## Recommended Next Lesson
**"Writing Your First Automated Test"** - Now that you have proper structure, learn to write effective, maintainable test cases that leverage your organized repository.