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.
Real-World Workflow: Managing Test Development Across All Three Areas
Why This Matters
You’ve just spent the morning fixing a critical test flakiness issue in your login tests. While debugging, you also experimented with a new test data approach, added some temporary logging statements, and made unrelated improvements to your API tests. Now you’re ready to commit your changes, but here’s the problem: committing everything together creates a messy, unclear history that will confuse your team and make code reviews painful.
This is where most test engineers struggle with Git. In theory, you understand the three areas—Working Directory, Staging Area, and Repository. But in practice, you need to know how to use them to maintain a clean, professional commit history that makes your test changes easy to review, understand, and potentially revert.
Real-world scenarios where this workflow saves you:
- Selective commits: You’ve modified five test files, but only three fixes are related to the same bug. You need to commit them separately with clear messages.
- Code review preparation: Your reviewer shouldn’t have to untangle unrelated test changes mixed into one commit.
- Emergency rollbacks: When a test breaks production verification, you need commits organized logically so you can revert just the problematic changes.
- Collaboration clarity: Your teammates can understand exactly what each commit accomplishes without deciphering a jumble of unrelated test modifications.
Common pain points this lesson addresses:
- “I accidentally committed debug code and test data experiments along with my real test fixes”
- “My commits are too large and reviewers can’t easily understand what changed”
- “I don’t know how to commit only some of my test file changes”
- “I staged the wrong file and don’t know how to unstage it”
- “I can’t tell what’s about to be committed versus what’s still in progress”
What You’ll Accomplish
By the end of this lesson, you’ll master the practical, day-to-day workflow that professional test engineers use to manage their test code changes. This isn’t about memorizing commands—it’s about developing a systematic approach to organizing your work.
Here’s how we’ll build your workflow mastery:
-
Implement a complete test development workflow - You’ll walk through a realistic scenario where you’re developing multiple test features simultaneously, learning to use all three Git areas in harmony to keep your work organized.
-
Selectively stage specific changes - You’ll learn multiple techniques for the
git add
command: staging individual files, using patterns to stage groups of related tests, staging specific directories, and even staging portions of files when needed. -
Create meaningful, focused commits - You’ll practice composing commits that each tell a clear story, grouping related test changes together while keeping unrelated changes separate for future commits.
-
Inspect your work at every stage - You’ll master using
git status
to see the big picture andgit diff
(with variations) to examine exactly what’s changed in your working directory, what’s staged for commit, and what’s already committed. -
Correct staging mistakes confidently - You’ll learn to unstage files you didn’t mean to add, recover from accidental staging operations, and adjust your staged changes before committing.
-
Navigate strategically between areas - Most importantly, you’ll develop the mental model for when and why to move test files between areas, making intentional decisions about organizing your changes.
This lesson focuses on the practical workflow you’ll use every single day as a test automation engineer. You’ll work through hands-on exercises that simulate real test development scenarios, building muscle memory for the commands and, more critically, judgment for using them effectively.
Core Content
Core Content: Real-World Workflow: Managing Test Development Across All Three Areas
Core Concepts Explained
Understanding the Three Testing Areas
In professional test automation, development spans three critical areas that work together:
- Unit Testing - Testing individual functions/methods in isolation
- API Testing - Testing backend services and endpoints
- UI Testing - Testing the complete user interface through a browser
A real-world workflow integrates all three areas to provide comprehensive test coverage. Each area has distinct purposes but shares common development practices.
The Integrated Testing Workflow
graph TD
A[Feature Development] --> B[Write Unit Tests]
B --> C[Write API Tests]
C --> D[Write UI Tests]
D --> E[Run All Test Suites]
E --> F{All Pass?}
F -->|No| G[Debug & Fix]
G --> E
F -->|Yes| H[Commit & Push]
H --> I[CI Pipeline Runs]
I --> J{CI Pass?}
J -->|Yes| K[Merge to Main]
J -->|No| G
Test Development Lifecycle
Step 1: Plan Your Test Coverage
Before writing tests, identify what needs testing at each level:
- Unit Level: Business logic, calculations, validations
- API Level: Request/response handling, status codes, data transformations
- UI Level: User workflows, form submissions, navigation
Step 2: Start with Unit Tests
Unit tests are fastest and should form the foundation:
// utils/calculator.js
function calculateTotal(items, taxRate = 0.08) {
if (!Array.isArray(items)) {
throw new Error('Items must be an array');
}
const subtotal = items.reduce((sum, item) => sum + item.price, 0);
const tax = subtotal * taxRate;
return subtotal + tax;
}
module.exports = { calculateTotal };
// tests/unit/calculator.test.js
const { calculateTotal } = require('../../utils/calculator');
describe('Calculator Utils', () => {
test('calculates total with tax', () => {
const items = [
{ name: 'Item 1', price: 10 },
{ name: 'Item 2', price: 20 }
];
const total = calculateTotal(items, 0.10);
expect(total).toBe(33); // 30 + 3 tax
});
test('throws error for invalid input', () => {
expect(() => calculateTotal('not an array')).toThrow('Items must be an array');
});
});
Step 3: Build API Tests
API tests verify backend functionality:
// tests/api/products.test.js
const request = require('supertest');
const app = require('../../src/app');
describe('Products API', () => {
let productId;
test('POST /api/products - creates new product', async () => {
const response = await request(app)
.post('/api/products')
.send({
name: 'Test Product',
price: 29.99,
stock: 100
})
.expect('Content-Type', /json/)
.expect(201);
expect(response.body).toHaveProperty('id');
expect(response.body.name).toBe('Test Product');
productId = response.body.id;
});
test('GET /api/products/:id - retrieves product', async () => {
const response = await request(app)
.get(`/api/products/${productId}`)
.expect(200);
expect(response.body.name).toBe('Test Product');
expect(response.body.price).toBe(29.99);
});
test('PUT /api/products/:id - updates product', async () => {
const response = await request(app)
.put(`/api/products/${productId}`)
.send({ price: 24.99 })
.expect(200);
expect(response.body.price).toBe(24.99);
});
});
Step 4: Create UI Tests
UI tests validate the complete user experience:
// tests/ui/checkout.test.js
const { test, expect } = require('@playwright/test');
test.describe('Checkout Workflow', () => {
test('complete purchase flow', async ({ page }) => {
// Navigate to product page
await page.goto('https://practiceautomatedtesting.com/products');
// Add product to cart
await page.locator('[data-testid="product-1"]').click();
await page.locator('button:has-text("Add to Cart")').click();
// Verify cart badge updates
const cartBadge = page.locator('[data-testid="cart-count"]');
await expect(cartBadge).toHaveText('1');
// Proceed to checkout
await page.locator('[data-testid="cart-icon"]').click();
await page.locator('button:has-text("Checkout")').click();
// Fill checkout form
await page.fill('[name="email"]', 'test@example.com');
await page.fill('[name="firstName"]', 'John');
await page.fill('[name="lastName"]', 'Doe');
await page.fill('[name="address"]', '123 Main St');
// Submit order
await page.locator('button:has-text("Place Order")').click();
// Verify success
await expect(page.locator('.success-message')).toBeVisible();
await expect(page.locator('.order-confirmation')).toContainText('Order #');
});
});
Organizing Your Test Suite
Project Structure:
project/
├── tests/
│ ├── unit/
│ │ ├── calculator.test.js
│ │ └── validators.test.js
│ ├── api/
│ │ ├── products.test.js
│ │ └── users.test.js
│ └── ui/
│ ├── checkout.test.js
│ └── navigation.test.js
├── src/
│ ├── utils/
│ └── api/
├── package.json
└── playwright.config.js
Running Tests Across All Areas
Package.json Scripts:
{
"scripts": {
"test": "npm run test:unit && npm run test:api && npm run test:ui",
"test:unit": "jest tests/unit",
"test:api": "jest tests/api",
"test:ui": "playwright test",
"test:unit:watch": "jest tests/unit --watch",
"test:coverage": "jest --coverage",
"test:ui:headed": "playwright test --headed"
}
}
Running the complete test suite:
$ npm test
> test:unit
PASS tests/unit/calculator.test.js
Calculator Utils
✓ calculates total with tax (3 ms)
✓ throws error for invalid input (1 ms)
Tests: 2 passed, 2 total
> test:api
PASS tests/api/products.test.js
Products API
✓ POST /api/products - creates new product (125 ms)
✓ GET /api/products/:id - retrieves product (45 ms)
✓ PUT /api/products/:id - updates product (67 ms)
Tests: 3 passed, 3 total
> test:ui
Running 1 test using 1 worker
✓ checkout.test.js:3:5 › complete purchase flow (2.5s)
1 passed (3s)
Workflow Integration Pattern
Daily Development Workflow:
graph LR
A[Pull Latest Code] --> B[Write Unit Tests]
B --> C[Implement Feature]
C --> D[Run Unit Tests]
D --> E{Pass?}
E -->|No| C
E -->|Yes| F[Write API Tests]
F --> G[Run API Tests]
G --> H{Pass?}
H -->|No| F
H -->|Yes| I[Write UI Tests]
I --> J[Run All Tests]
J --> K{All Pass?}
K -->|No| L[Debug]
L --> J
K -->|Yes| M[Commit Changes]
Shared Test Utilities
Create reusable helpers across test areas:
// tests/helpers/testData.js
module.exports = {
validProduct: {
name: 'Test Product',
price: 29.99,
stock: 100
},
validUser: {
email: 'test@example.com',
firstName: 'John',
lastName: 'Doe'
},
generateRandomEmail: () => {
return `test.${Date.now()}@example.com`;
}
};
// tests/helpers/apiHelpers.js
const request = require('supertest');
const app = require('../../src/app');
async function createProduct(productData) {
const response = await request(app)
.post('/api/products')
.send(productData);
return response.body;
}
async function cleanupTestData(productId) {
await request(app)
.delete(`/api/products/${productId}`);
}
module.exports = { createProduct, cleanupTestData };
Using shared helpers:
// tests/ui/productSearch.test.js
const { test, expect } = require('@playwright/test');
const { createProduct, cleanupTestData } = require('../helpers/apiHelpers');
const { validProduct } = require('../helpers/testData');
test('search finds newly created product', async ({ page }) => {
// Setup: Create product via API
const product = await createProduct(validProduct);
try {
// Test: Search via UI
await page.goto('https://practiceautomatedtesting.com/search');
await page.fill('[data-testid="search-input"]', product.name);
await page.click('[data-testid="search-button"]');
// Verify
await expect(page.locator('.search-results')).toContainText(product.name);
} finally {
// Cleanup: Remove test data
await cleanupTestData(product.id);
}
});
Common Mistakes Section
Mistake 1: Running All Tests Every Time
Wrong:
# Running full UI suite for every small change
$ npm run test:ui
# Takes 5+ minutes
Right:
# Run unit tests during development (fast feedback)
$ npm run test:unit:watch
# Run API tests when endpoints change
$ npm run test:api
# Run UI tests before commit/push
$ npm run test:ui
Mistake 2: Duplicate Test Logic
Wrong:
// Repeating same setup in multiple tests
test('test 1', async () => {
const response = await request(app).post('/api/products').send({...});
// test logic
});
test('test 2', async () => {
const response = await request(app).post('/api/products').send({...});
// test logic
});
Right:
// Use beforeEach for common setup
describe('Product Tests', () => {
let productId;
beforeEach(async () => {
const response = await createProduct(validProduct);
productId = response.id;
});
afterEach(async () => {
await cleanupTestData(productId);
});
test('test 1', async () => {
// test logic using productId
});
});
Mistake 3: Not Isolating Test Areas
Wrong:
// Unit test making HTTP calls
test('calculate order total', async () => {
const products = await fetch('/api/products'); // ❌ API call in unit test
const total = calculateTotal(products);
});
Right:
// Unit test with mocked data
test('calculate order total', () => {
const products = [{ price: 10 }, { price: 20 }]; // ✅ Mock data
const total = calculateTotal(products);
expect(total).toBe(32.4);
});
Debugging Cross-Area Issues
When UI tests fail but API tests pass:
// Add logging to understand the flow
test('checkout flow', async ({ page }) => {
// Enable request logging
page.on('request', request => {
console.log('Request:', request.url());
});
page.on('response', response => {
console.log('Response:', response.status(), response.url());
});
// Run your test
await page.goto('https://practiceautomatedtesting.com/checkout');
// Continue test...
});
When tests pass locally but fail in CI:
// Add explicit waits and retries
test('flaky element', async ({ page }) => {
await page.goto('https://practiceautomatedtesting.com/products');
// Wait for network idle before interacting
await page.waitForLoadState('networkidle');
// Use explicit waits
await page.waitForSelector('[data-testid="product-1"]', {
state: 'visible',
timeout: 10000
});
await page.click('[data-testid="product-1"]');
});
Mistake 4: Not Managing Test Data
Problem: Tests interfere with each other
Solution: Isolate test data
// Generate unique data per test run
const testData = {
email: `test.${Date.now()}@example.com`,
productName: `Test Product ${Math.random().toString(36).substr(2, 9)}`
};
// Clean up after tests
afterAll(async () => {
await cleanupTestData();
});
Hands-On Practice
Hands-On Exercise
Task: Build a Multi-Layer Test Suite for an E-commerce Checkout Flow
You’ll create a comprehensive test automation suite that spans unit, integration, and E2E testing for a shopping cart checkout feature. This exercise tests your ability to design, implement, and coordinate tests across all three testing layers.
Scenario
You’re testing a checkout system with:
- Unit layer: Payment validation logic
- Integration layer: Cart service + payment gateway API
- E2E layer: Complete user checkout workflow
Step-by-Step Instructions
Step 1: Analyze and Plan (15 minutes)
- Review the three components provided in the starter code
- Identify what should be tested at each layer
- Create a test plan document listing:
- 3 unit tests
- 2 integration tests
- 1 E2E test
- Dependencies between layers
Step 2: Implement Unit Tests (20 minutes)
- Create unit tests for the
PaymentValidator
class - Test: valid card format, expired card detection, minimum amount validation
- Use mocks for any external dependencies
- Ensure tests run in < 100ms
Step 3: Build Integration Tests (25 minutes)
- Test
CartService
interaction withPaymentGateway
API - Mock the actual payment processor but test real HTTP calls
- Verify: successful payment flow, failed payment handling
- Use test fixtures for cart data
Step 4: Create E2E Test (30 minutes)
- Simulate complete user journey: add item → checkout → payment → confirmation
- Use a test database and mock payment gateway
- Verify UI updates and database state
- Include proper setup and teardown
Step 5: Coordinate Test Execution (10 minutes)
- Set up test execution order (unit → integration → E2E)
- Configure test runner to fail fast on lower-layer failures
- Create a test report showing coverage across all layers
Starter Code
// src/paymentValidator.js
class PaymentValidator {
validateCardNumber(cardNumber) {
// Basic Luhn algorithm check
return cardNumber.length === 16 && /^\d+$/.test(cardNumber);
}
isCardExpired(expiryDate) {
const [month, year] = expiryDate.split('/');
const expiry = new Date(2000 + parseInt(year), parseInt(month) - 1);
return expiry < new Date();
}
validateAmount(amount) {
return amount >= 0.50 && amount <= 10000;
}
}
// src/cartService.js
class CartService {
constructor(paymentGateway) {
this.gateway = paymentGateway;
this.items = [];
}
addItem(item) {
this.items.push(item);
}
getTotal() {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
async processCheckout(paymentDetails) {
const total = this.getTotal();
const result = await this.gateway.charge(total, paymentDetails);
if (result.success) {
this.items = [];
}
return result;
}
}
// src/paymentGateway.js (integration point)
class PaymentGateway {
async charge(amount, paymentDetails) {
// Calls external API
const response = await fetch('/api/payment', {
method: 'POST',
body: JSON.stringify({ amount, ...paymentDetails })
});
return response.json();
}
}
Expected Outcome
Your completed suite should:
- ✅ Have 6 total tests (3 unit, 2 integration, 1 E2E)
- ✅ Unit tests execute in parallel and complete in < 500ms total
- ✅ Integration tests properly mock external services
- ✅ E2E test covers happy path with all components
- ✅ Test execution stops if unit tests fail
- ✅ Generate a coverage report showing 80%+ code coverage
Solution Approach
Unit Tests Structure:
describe('PaymentValidator', () => {
test('validates correct card number');
test('rejects expired cards');
test('enforces amount limits');
});
Integration Tests Structure:
describe('CartService + PaymentGateway', () => {
test('successful payment clears cart');
test('failed payment keeps items in cart');
// Use nock or msw to mock HTTP calls
});
E2E Test Structure:
describe('Checkout Flow', () => {
beforeEach(() => {
// Setup test DB and mock payment API
});
test('complete purchase flow', async () => {
// 1. Add items to cart (UI)
// 2. Navigate to checkout
// 3. Enter payment details
// 4. Verify confirmation page
// 5. Check database for order record
});
});
Test Execution Config:
{
"testSequencer": "ordered",
"testMatch": [
"**/*.unit.test.js",
"**/*.integration.test.js",
"**/*.e2e.test.js"
],
"bail": 1
}
Key Takeaways
🎯 What You’ve Learned:
-
Test Pyramid in Practice: Unit tests form the foundation (fast, isolated), integration tests verify component interactions (moderate speed, focused scope), and E2E tests validate complete workflows (slower, comprehensive)
-
Strategic Test Placement: Not everything needs testing at every layer. Business logic belongs in unit tests, API contracts in integration tests, and user workflows in E2E tests—avoiding redundancy while maintaining coverage
-
Dependency Management: Lower-layer test failures should block higher-layer execution. A failing unit test indicates broken logic that will definitely fail in E2E tests, so fail fast and save time
-
Mock Strategically: Mock external dependencies in unit tests, mock only the outermost services in integration tests, and use test doubles for third-party services in E2E tests to ensure reliability
-
Coordinated Execution: Organizing tests by type and configuring proper execution order creates a feedback loop that guides development and debugging from specific (units) to general (user flows)
Next Steps
What to Practice
- Expand Your Test Suite: Add negative test cases (invalid inputs, network failures, race conditions) across all three layers
- Optimize Test Speed: Experiment with parallel execution for unit tests while keeping integration/E2E sequential
- Implement CI/CD Integration: Set up a pipeline that runs unit tests on every commit, integration tests on PR, and E2E tests before deployment
Related Topics to Explore
- Contract Testing: Learn Pact or similar tools to test API contracts between services
- Test Data Management: Explore fixtures, factories, and database seeding strategies
- Flaky Test Detection: Implement retry logic and monitoring for unstable E2E tests
- Visual Regression Testing: Add screenshot comparison tests to your E2E suite
- Performance Testing: Layer in load tests at the integration level for critical paths
- Mutation Testing: Use tools like Stryker to verify your test quality
Recommended Resources
- “Growing Object-Oriented Software, Guided by Tests” for advanced TDD patterns
- Martin Fowler’s testing blog posts on test doubles and contract testing
- Your organization’s test strategy documentation (align with team practices)