H M < >
CMU597: Industry Project - Lecture 8

Sprint 3: Refinement & Quality

Part A: Code Quality | Part B: Refactor & Test Sprint

James Williams

Birmingham Newman University

jwilliams@staff.newman.ac.uk

3-hour session • 2 parts • 2 team tasks

Session Timeline

Part A: Refactoring, Code Quality, Testing (90 minutes)

30 minutes: Lecture on refactoring, DRY, testing strategies

45 minutes: Task 1 - Code quality audit

15 minutes: Break

Part B: Refactor + Test Sprint (90 minutes)

30 minutes: Testing examples and refactoring patterns

45 minutes: Task 2 - Refactor code and write tests

15 minutes: Review and test execution

Learning Objectives

  • Understand code smells and how to identify them
  • Apply DRY (Don't Repeat Yourself) principle
  • Refactor code for better readability and maintainability
  • Write effective unit tests
  • Understand test-driven development (TDD) concepts
  • Implement logging for debugging and monitoring
  • Identify and fix bugs systematically
  • Improve code quality without breaking functionality

Why Code Quality Matters

Technical Debt: Poor code today = more work tomorrow.

Bad Code Costs:

  • Harder to understand
  • More bugs
  • Slower to modify
  • Difficult to test
  • Team frustration

Good Code Benefits:

  • Easy to read
  • Fewer bugs
  • Fast to change
  • Simple to test
  • Team confidence
Sprint 3 Goal: Pause feature development, improve what you have.

Common Code Smells

Signs your code needs refactoring:

  • Long Functions: Function does too much (> 50 lines)
  • Duplicated Code: Same logic in multiple places
  • Magic Numbers: Unexplained constants (e.g., `if (status === 3)`)
  • Long Parameter Lists: Functions with > 3-4 parameters
  • Nested Conditionals: if inside if inside if...
  • God Objects: Class/file that does everything
  • Dead Code: Unused variables, functions, imports
  • Poor Naming: Variables like `x`, `temp`, `data`

DRY: Don't Repeat Yourself

❌ Repeated Code:

// Validation repeated
if (!user.email || !user.email.includes('@')) {
  return 'Invalid email';
}

if (!admin.email || !admin.email.includes('@')) {
  return 'Invalid email';
}

if (!customer.email || !customer.email.includes('@')) {
  return 'Invalid email';
}

✓ DRY Solution:

// Extract to reusable function
function validateEmail(email) {
  if (!email || !email.includes('@')) {
    return 'Invalid email';
  }
  return null;
}

// Use everywhere
const userError = validateEmail(user.email);
const adminError = validateEmail(admin.email);
const customerError = validateEmail(customer.email);

Refactoring: Extract Functions

Before: Long Function

function processOrder(order) {
  // Validate
  if (!order.items || order.items.length === 0) {
    throw new Error('No items');
  }
  
  // Calculate total
  let total = 0;
  for (const item of order.items) {
    total += item.price * item.quantity;
  }
  
  // Apply discount
  if (order.coupon) {
    total = total * 0.9;
  }
  
  // Save to DB
  db.insert('orders', {
    ...order,
    total
  });
  
  // Send email
  sendEmail(order.email, 'Order confirmed');
}

After: Small Functions

function processOrder(order) {
  validateOrder(order);
  const total = calculateTotal(order);
  saveOrder(order, total);
  notifyCustomer(order);
}

function validateOrder(order) {
  if (!order.items?.length) {
    throw new Error('No items');
  }
}

function calculateTotal(order) {
  let total = order.items.reduce(
    (sum, item) => sum + item.price * item.quantity, 0
  );
  if (order.coupon) total *= 0.9;
  return total;
}

function saveOrder(order, total) {
  db.insert('orders', {...order, total});
}

function notifyCustomer(order) {
  sendEmail(order.email, 'Order confirmed');
}

Replace Magic Numbers with Named Constants

❌ Magic Numbers:

if (user.age < 18) {
  return 'Too young';
}

if (order.status === 3) {
  shipOrder(order);
}

setTimeout(checkStatus, 300000);

✓ Named Constants:

const MIN_AGE = 18;
if (user.age < MIN_AGE) {
  return 'Too young';
}

const ORDER_STATUS = {
  PENDING: 1,
  PAID: 2,
  SHIPPED: 3
};

if (order.status === ORDER_STATUS.SHIPPED) {
  shipOrder(order);
}

const FIVE_MINUTES = 5 * 60 * 1000;
setTimeout(checkStatus, FIVE_MINUTES);

Simplify Complex Conditionals

❌ Nested Mess:

if (user) {
  if (user.isActive) {
    if (user.hasPermission('admin')) {
      if (user.emailVerified) {
        return true;
      } else {
        return false;
      }
    } else {
      return false;
    }
  } else {
    return false;
  }
} else {
  return false;
}

✓ Early Returns:

function canAccessAdmin(user) {
  if (!user) return false;
  if (!user.isActive) return false;
  if (!user.hasPermission('admin')) return false;
  if (!user.emailVerified) return false;
  
  return true;
}

// Even better: single expression
function canAccessAdmin(user) {
  return user?.isActive && 
         user?.hasPermission('admin') && 
         user?.emailVerified;
}

Software Testing Fundamentals

Types of Tests:

  • Unit Tests: Test individual functions/methods in isolation
    Example: Test validateEmail() function
  • Integration Tests: Test multiple components together
    Example: Test API endpoint + database
  • End-to-End (E2E) Tests: Test entire user flow
    Example: Complete registration process in browser

Testing Pyramid:

        ╱╲       Few E2E Tests (slow, expensive)
       ╱  ╲
      ╱────╲     Some Integration Tests
     ╱      ╲
    ╱────────╲   Many Unit Tests (fast, cheap)
                    

Writing Unit Tests with Jest

// Function to test
function calculateDiscount(price, discountPercent) {
  if (price < 0 || discountPercent < 0 || discountPercent > 100) {
    throw new Error('Invalid input');
  }
  return price * (1 - discountPercent / 100);
}

// Unit tests
describe('calculateDiscount', () => {
  test('applies 10% discount correctly', () => {
    expect(calculateDiscount(100, 10)).toBe(90);
  });
  
  test('applies 50% discount correctly', () => {
    expect(calculateDiscount(200, 50)).toBe(100);
  });
  
  test('handles 0% discount', () => {
    expect(calculateDiscount(100, 0)).toBe(100);
  });
  
  test('throws error for negative price', () => {
    expect(() => calculateDiscount(-10, 10)).toThrow('Invalid input');
  });
  
  test('throws error for discount > 100%', () => {
    expect(() => calculateDiscount(100, 150)).toThrow('Invalid input');
  });
});

What to Test?

Test Coverage Guidelines:

  • Happy Path: Normal, expected usage
  • Edge Cases: Boundary values (0, MAX, empty, null)
  • Error Cases: Invalid inputs, failures
  • Business Logic: Critical calculations, validations

Example: Testing Login Function

  • ✓ Valid email and password (happy path)
  • ✓ Invalid email format
  • ✓ Wrong password
  • ✓ Non-existent user
  • ✓ Empty email or password
  • ✓ Account locked/disabled

Implementing Logging

Logging: Recording what happens in your application for debugging and monitoring.

Log Levels:

  • ERROR: Critical failures
  • WARN: Potential issues
  • INFO: Important events
  • DEBUG: Detailed diagnostic info
// Using Winston logger
const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

// Usage in code
logger.info('User logged in', { userId: 123, email: 'user@example.com' });
logger.error('Database connection failed', { error: err.message });
logger.warn('API rate limit approaching', { remaining: 10 });

Task 1: Conduct Code Quality Audit

Instructions (Work in your project teams):

  1. Identify Code Smells (15 min):
    • Review your codebase together
    • Look for: duplicated code, long functions, magic numbers, poor naming
    • List specific files and line numbers
  2. Assess Test Coverage (10 min):
    • What functions have tests?
    • What critical logic is untested?
    • List functions that need tests
  3. Find Bugs (10 min):
    • Known bugs from testing
    • Edge cases not handled
    • Missing error handling
  4. Prioritize (10 min):
    • High: Critical bugs, duplicated logic
    • Medium: Long functions, magic numbers
    • Low: Naming improvements
    • Create docs/code-quality-audit.md

Time: 45 minutes

Deliverable: Code quality audit with prioritized improvements

Break Time

15 Minutes

Take a break. Next: Refactoring and testing!

Part B: Refactor + Test Sprint

Part B: Refactor Code & Write Tests

Now implement improvements from your audit!

Sprint Goals:

  • Refactor at least 2-3 code smells
  • Write unit tests for 3-5 critical functions
  • Fix 1-2 known bugs
  • Add logging to key operations
  • Verify nothing breaks (regression testing)

How to Refactor Safely

Golden Rule:

Don't change behavior while refactoring! Refactoring = improving structure WITHOUT changing what code does.

Safe Refactoring Process:

  1. Write tests first (or verify existing tests pass)
  2. Make small change (extract function, rename variable)
  3. Run tests to ensure still passing
  4. Repeat until code is clean
  5. Commit after each successful refactor
Git Tip: Make separate commits for refactoring vs new features.

Test-Driven Development (TDD)

TDD: Write test BEFORE writing code.

TDD cycle (Red-Green-Refactor):

  1. Red: Write failing test
  2. Green: Write minimum code to pass
  3. Refactor: Clean up code
  4. Repeat
// 1. RED: Write test first (fails - function doesn't exist yet)
test('isPalindrome returns true for "racecar"', () => {
  expect(isPalindrome('racecar')).toBe(true);
});

// 2. GREEN: Write minimum code to pass
function isPalindrome(str) {
  return str === str.split('').reverse().join('');
}

// 3. REFACTOR: Improve code
function isPalindrome(str) {
  const cleaned = str.toLowerCase().replace(/[^a-z]/g, '');
  return cleaned === cleaned.split('').reverse().join('');
}

Testing API Endpoints

// Using Supertest with Express
const request = require('supertest');
const app = require('./app');

describe('POST /api/users', () => {
  test('creates user with valid data', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({
        email: 'test@example.com',
        name: 'Test User'
      });
    
    expect(response.status).toBe(201);
    expect(response.body).toHaveProperty('id');
    expect(response.body.email).toBe('test@example.com');
  });
  
  test('returns 400 for invalid email', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({
        email: 'invalid-email',
        name: 'Test User'
      });
    
    expect(response.status).toBe(400);
    expect(response.body.error).toMatch(/invalid email/i);
  });
});

Mocking Dependencies in Tests

Mock: Replace real dependency (DB, API) with fake version for testing.
// Code that calls database
async function getUser(id) {
  const user = await db.query('SELECT * FROM users WHERE id = $1', [id]);
  return user;
}

// Test with mocked database
jest.mock('./db');
const db = require('./db');

test('getUser returns user from database', async () => {
  // Mock the query function
  db.query.mockResolvedValue({
    id: 1,
    name: 'Tom',
    email: 'Tom@example.com'
  });
  
  const user = await getUser(1);
  
  expect(user.name).toBe('Tom');
  expect(db.query).toHaveBeenCalledWith(
    'SELECT * FROM users WHERE id = $1',
    [1]
  );
});

Debugging Techniques

Systematic Debugging:

  1. Reproduce: Can you make bug happen consistently?
  2. Isolate: Where exactly does it fail?
  3. Hypothesize: What do you think causes it?
  4. Test: Add logging, use debugger, check values
  5. Fix: Make smallest change that fixes it
  6. Verify: Test fix doesn't break anything else

Debugging Tools:

  • console.log(): Quick inspection
  • debugger; Break point in browser DevTools
  • VSCode debugger: Step through code
  • Network tab: Inspect API calls
  • Error stack traces: Follow the path

Running Tests Automatically (CI)

# .github/workflows/test.yml - GitHub Actions
name: Run Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v2
      
      - name: Setup Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '16'
      
      - name: Install dependencies
        run: npm install
      
      - name: Run tests
        run: npm test
      
      - name: Check test coverage
        run: npm run test:coverage

Benefit: Tests run automatically on every push/PR!

Post-Refactor Code Review Checklist

Before merging refactors:

  • ✓ All existing tests still pass
  • ✓ New tests added where needed
  • ✓ Code is more readable than before
  • ✓ No functionality changed (only structure)
  • ✓ No new bugs introduced
  • ✓ Commit messages explain WHY refactored
  • ✓ Team reviewed changes
PR Description: Explain what you refactored and why. Include before/after code snippets.

Task 2: Refactor Code & Write Tests

Instructions (Work in your project teams):

  1. Select Items from Audit (5 min):
    • Choose 2-3 refactors and 3-5 functions to test
    • Divide among team members
  2. Write Tests First (15 min):
    • Write unit tests for functions you'll refactor
    • Ensure tests pass with current code
  3. Refactor (20 min):
    • Extract functions, remove duplication, add constants
    • Run tests after each change
    • Commit after successful refactor
  4. Add Logging (5 min):
    • Add logging to critical operations
    • Log errors with context

Time: 45 minutes

Deliverable: Refactored code with tests, all tests passing

Lecture 8 Summary

  • Code quality prevents technical debt and improves maintainability
  • Code smells indicate areas needing refactoring
  • DRY principle eliminates code duplication
  • Extract functions to break down complex logic
  • Replace magic numbers with named constants
  • Unit tests verify individual functions work correctly
  • Test-Driven Development writes tests before code
  • Logging helps debug issues and monitor applications
  • Refactor safely by running tests frequently

Next Lecture:

Sprint 3 Review & Documentation Sprint
Creating technical documentation, architecture diagrams, and API documentation

Before Next Lecture

  1. Complete Refactoring:
    • Finish items from audit
    • Ensure all tests pass
    • Create PRs for refactors
  2. Fix Bugs:
    • Address known bugs from audit
    • Add tests to prevent regression
  3. Test Coverage:
    • Run coverage report: npm test -- --coverage
    • Aim for > 70% coverage on critical code
  4. Submit to Moodle:
    • Code quality audit document
    • Test coverage report screenshot
    • List of refactorings completed