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:

  1. 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.

  2. 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.

  3. 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.

  4. Inspect your work at every stage - You’ll master using git status to see the big picture and git diff (with variations) to examine exactly what’s changed in your working directory, what’s staged for commit, and what’s already committed.

  5. 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.

  6. 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:

  1. Unit Testing - Testing individual functions/methods in isolation
  2. API Testing - Testing backend services and endpoints
  3. 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)

  1. Review the three components provided in the starter code
  2. Identify what should be tested at each layer
  3. 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)

  1. Create unit tests for the PaymentValidator class
  2. Test: valid card format, expired card detection, minimum amount validation
  3. Use mocks for any external dependencies
  4. Ensure tests run in < 100ms

Step 3: Build Integration Tests (25 minutes)

  1. Test CartService interaction with PaymentGateway API
  2. Mock the actual payment processor but test real HTTP calls
  3. Verify: successful payment flow, failed payment handling
  4. Use test fixtures for cart data

Step 4: Create E2E Test (30 minutes)

  1. Simulate complete user journey: add item → checkout → payment → confirmation
  2. Use a test database and mock payment gateway
  3. Verify UI updates and database state
  4. Include proper setup and teardown

Step 5: Coordinate Test Execution (10 minutes)

  1. Set up test execution order (unit → integration → E2E)
  2. Configure test runner to fail fast on lower-layer failures
  3. 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

  1. Expand Your Test Suite: Add negative test cases (invalid inputs, network failures, race conditions) across all three layers
  2. Optimize Test Speed: Experiment with parallel execution for unit tests while keeping integration/E2E sequential
  3. Implement CI/CD Integration: Set up a pipeline that runs unit tests on every commit, integration tests on PR, and E2E tests before deployment
  • 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
  • “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)