James Williams
Birmingham Newman University
jwilliams@staff.newman.ac.uk
3-hour session • 2 parts • 2 team tasks
30 minutes: Lecture on refactoring, DRY, testing strategies
45 minutes: Task 1 - Code quality audit
15 minutes: Break
30 minutes: Testing examples and refactoring patterns
45 minutes: Task 2 - Refactor code and write tests
15 minutes: Review and test execution
// 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';
}
// 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);
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');
}
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');
}
if (user.age < 18) {
return 'Too young';
}
if (order.status === 3) {
shipOrder(order);
}
setTimeout(checkStatus, 300000);
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);
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;
}
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;
}
╱╲ Few E2E Tests (slow, expensive)
╱ ╲
╱────╲ Some Integration Tests
╱ ╲
╱────────╲ Many Unit Tests (fast, cheap)
// 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');
});
});
// 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 });
Time: 45 minutes
Deliverable: Code quality audit with prioritized improvements
Take a break. Next: Refactoring and testing!
Part B: Refactor + Test Sprint
// 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('');
}
// 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);
});
});
// 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]
);
});
# .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!
Time: 45 minutes
Deliverable: Refactored code with tests, all tests passing
Sprint 3 Review & Documentation Sprint
Creating technical documentation, architecture diagrams, and API documentation