Testing Basics
Learning Objectives
- By the end of this lesson, you will be able to:
- - Understand why testing is important
- - Know different types of testing
- - Understand test-driven development (TDD)
- - Write basic tests
- - Structure test files
- - Run tests
- - Build confidence in your code
Lesson 22.1: Testing Basics
Learning Objectives
By the end of this lesson, you will be able to:
- Understand why testing is important
- Know different types of testing
- Understand test-driven development (TDD)
- Write basic tests
- Structure test files
- Run tests
- Build confidence in your code
Introduction to Testing
Testing is the process of verifying that code works as expected. It's essential for building reliable applications.
Why Test?
- Confidence: Know your code works
- Documentation: Tests document how code should work
- Refactoring: Safe to change code with tests
- Bug Prevention: Catch bugs early
- Quality: Ensures code quality
- Professional: Industry standard practice
Benefits of Testing
// Without tests: Unsure if code works
function add(a, b) {
return a + b;
}
// Does it work? Maybe?
// With tests: Confident code works
function add(a, b) {
return a + b;
}
// Test: add(2, 3) === 5 ✓
Types of Testing
Unit Testing
Tests individual units (functions, classes) in isolation.
// Unit test example
function multiply(a, b) {
return a * b;
}
// Test
test('multiply should return product', () => {
expect(multiply(2, 3)).toBe(6);
expect(multiply(0, 5)).toBe(0);
expect(multiply(-1, 5)).toBe(-5);
});
Integration Testing
Tests how multiple units work together.
// Integration test example
class UserService {
constructor(userRepository) {
this.userRepository = userRepository;
}
async createUser(userData) {
// Validate
if (!userData.email) {
throw new Error('Email required');
}
// Save
return await this.userRepository.save(userData);
}
}
// Test: UserService + UserRepository work together
test('createUser should save user', async () => {
let userRepository = new UserRepository();
let userService = new UserService(userRepository);
let user = await userService.createUser({
email: 'test@example.com',
name: 'Test User'
});
expect(user.email).toBe('test@example.com');
});
End-to-End (E2E) Testing
Tests the entire application from user's perspective.
// E2E test example (conceptual)
test('user can login and view dashboard', async () => {
// 1. Open browser
await page.goto('http://localhost:3000');
// 2. Login
await page.fill('#email', 'user@example.com');
await page.fill('#password', 'password');
await page.click('#login-button');
// 3. Verify dashboard
await expect(page.locator('#dashboard')).toBeVisible();
});
Test Pyramid
/\
/E2E\ Few, slow, expensive
/------\
/Integration\ Some, medium speed
/------------\
/ Unit Tests \ Many, fast, cheap
/----------------\
- Many Unit Tests: Fast, test individual functions
- Some Integration Tests: Test components together
- Few E2E Tests: Test critical user flows
Test-Driven Development (TDD)
What is TDD?
TDD is a development approach where you write tests before writing code.
TDD Cycle
1. Red: Write failing test
2. Green: Write minimal code to pass
3. Refactor: Improve code while keeping tests green
TDD Example
// Step 1: Red - Write failing test
test('calculateTotal should sum prices', () => {
let items = [
{ price: 10 },
{ price: 20 },
{ price: 30 }
];
expect(calculateTotal(items)).toBe(60);
});
// Test fails: calculateTotal is not defined
// Step 2: Green - Write minimal code
function calculateTotal(items) {
return 60; // Hardcoded to pass test
}
// Test passes
// Step 3: Refactor - Write real implementation
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price, 0);
}
// Test still passes, code is correct
Benefits of TDD
- Better Design: Forces you to think about interface first
- Confidence: Know code works from start
- Documentation: Tests document expected behavior
- Refactoring: Safe to refactor with tests
Writing Basic Tests
Test Structure
// Basic test structure
test('test description', () => {
// Arrange: Set up test data
let input = 5;
// Act: Execute code being tested
let result = double(input);
// Assert: Verify result
expect(result).toBe(10);
});
Arrange-Act-Assert Pattern
// Arrange: Set up
let numbers = [1, 2, 3, 4, 5];
// Act: Execute
let sum = numbers.reduce((a, b) => a + b, 0);
// Assert: Verify
expect(sum).toBe(15);
Test Cases
// Test different cases
test('add handles positive numbers', () => {
expect(add(2, 3)).toBe(5);
});
test('add handles zero', () => {
expect(add(5, 0)).toBe(5);
});
test('add handles negative numbers', () => {
expect(add(-2, 3)).toBe(1);
});
test('add handles decimals', () => {
expect(add(1.5, 2.5)).toBe(4);
});
Edge Cases
// Test edge cases
test('divide handles division by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
test('findUser handles empty array', () => {
expect(findUser([], 1)).toBeNull();
});
test('validateEmail handles null', () => {
expect(validateEmail(null)).toBe(false);
});
Test Organization
Test Files
// math.test.js
// Tests for math.js
import { add, subtract, multiply, divide } from './math.js';
describe('Math functions', () => {
test('add should sum numbers', () => {
expect(add(2, 3)).toBe(5);
});
test('subtract should subtract numbers', () => {
expect(subtract(5, 3)).toBe(2);
});
test('multiply should multiply numbers', () => {
expect(multiply(2, 3)).toBe(6);
});
test('divide should divide numbers', () => {
expect(divide(10, 2)).toBe(5);
});
});
Test Suites
// Group related tests
describe('UserService', () => {
describe('createUser', () => {
test('should create user with valid data', () => {
// Test
});
test('should throw error with invalid email', () => {
// Test
});
});
describe('getUser', () => {
test('should return user by id', () => {
// Test
});
test('should return null if not found', () => {
// Test
});
});
});
Practice Exercise
Exercise: Writing Basic Tests
Objective: Practice writing basic tests, understanding test structure, and TDD.
Instructions:
- Create a JavaScript file with functions to test
- Create a test file
- Practice:
- Writing unit tests
- Testing different cases
- Testing edge cases
- Using TDD approach
Example Solution:
// calculator.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
function multiply(a, b) {
return a * b;
}
function divide(a, b) {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price, 0);
}
function validateEmail(email) {
if (!email) return false;
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
function findUser(users, id) {
return users.find(user => user.id === id) || null;
}
module.exports = {
add,
subtract,
multiply,
divide,
calculateTotal,
validateEmail,
findUser
};
// calculator.test.js
const {
add,
subtract,
multiply,
divide,
calculateTotal,
validateEmail,
findUser
} = require('./calculator');
console.log("=== Testing Basics Practice ===");
console.log("\n=== Basic Tests ===");
// Test add
function testAdd() {
console.log('Testing add...');
// Positive numbers
if (add(2, 3) !== 5) {
console.error('FAIL: add(2, 3) should be 5');
return false;
}
// Zero
if (add(5, 0) !== 5) {
console.error('FAIL: add(5, 0) should be 5');
return false;
}
// Negative numbers
if (add(-2, 3) !== 1) {
console.error('FAIL: add(-2, 3) should be 1');
return false;
}
// Decimals
if (add(1.5, 2.5) !== 4) {
console.error('FAIL: add(1.5, 2.5) should be 4');
return false;
}
console.log('✓ add tests passed');
return true;
}
// Test subtract
function testSubtract() {
console.log('Testing subtract...');
if (subtract(5, 3) !== 2) {
console.error('FAIL: subtract(5, 3) should be 2');
return false;
}
if (subtract(0, 5) !== -5) {
console.error('FAIL: subtract(0, 5) should be -5');
return false;
}
console.log('✓ subtract tests passed');
return true;
}
// Test multiply
function testMultiply() {
console.log('Testing multiply...');
if (multiply(2, 3) !== 6) {
console.error('FAIL: multiply(2, 3) should be 6');
return false;
}
if (multiply(0, 5) !== 0) {
console.error('FAIL: multiply(0, 5) should be 0');
return false;
}
if (multiply(-2, 3) !== -6) {
console.error('FAIL: multiply(-2, 3) should be -6');
return false;
}
console.log('✓ multiply tests passed');
return true;
}
// Test divide
function testDivide() {
console.log('Testing divide...');
if (divide(10, 2) !== 5) {
console.error('FAIL: divide(10, 2) should be 5');
return false;
}
// Test division by zero
try {
divide(10, 0);
console.error('FAIL: divide(10, 0) should throw error');
return false;
} catch (error) {
if (error.message !== 'Division by zero') {
console.error('FAIL: Error message should be "Division by zero"');
return false;
}
}
console.log('✓ divide tests passed');
return true;
}
console.log();
console.log("=== Edge Cases ===");
// Test calculateTotal
function testCalculateTotal() {
console.log('Testing calculateTotal...');
// Normal case
let items1 = [
{ price: 10 },
{ price: 20 },
{ price: 30 }
];
if (calculateTotal(items1) !== 60) {
console.error('FAIL: calculateTotal should be 60');
return false;
}
// Empty array
if (calculateTotal([]) !== 0) {
console.error('FAIL: calculateTotal([]) should be 0');
return false;
}
// Single item
if (calculateTotal([{ price: 5 }]) !== 5) {
console.error('FAIL: calculateTotal with single item should be 5');
return false;
}
console.log('✓ calculateTotal tests passed');
return true;
}
// Test validateEmail
function testValidateEmail() {
console.log('Testing validateEmail...');
// Valid emails
if (!validateEmail('test@example.com')) {
console.error('FAIL: "test@example.com" should be valid');
return false;
}
if (!validateEmail('user.name@example.co.uk')) {
console.error('FAIL: "user.name@example.co.uk" should be valid');
return false;
}
// Invalid emails
if (validateEmail('invalid')) {
console.error('FAIL: "invalid" should be invalid');
return false;
}
if (validateEmail('invalid@')) {
console.error('FAIL: "invalid@" should be invalid');
return false;
}
if (validateEmail('@example.com')) {
console.error('FAIL: "@example.com" should be invalid');
return false;
}
// Edge cases
if (validateEmail(null)) {
console.error('FAIL: null should be invalid');
return false;
}
if (validateEmail('')) {
console.error('FAIL: empty string should be invalid');
return false;
}
console.log('✓ validateEmail tests passed');
return true;
}
// Test findUser
function testFindUser() {
console.log('Testing findUser...');
let users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Charlie' }
];
// Found
let user = findUser(users, 2);
if (!user || user.name !== 'Bob') {
console.error('FAIL: findUser should find user with id 2');
return false;
}
// Not found
if (findUser(users, 99) !== null) {
console.error('FAIL: findUser should return null for non-existent id');
return false;
}
// Empty array
if (findUser([], 1) !== null) {
console.error('FAIL: findUser should return null for empty array');
return false;
}
console.log('✓ findUser tests passed');
return true;
}
console.log();
console.log("=== TDD Example ===");
// TDD: Test first, then implement
function testReverseString() {
console.log('Testing reverseString (TDD example)...');
// Step 1: Red - Write failing test
// We expect reverseString to exist and work
try {
if (reverseString('hello') !== 'olleh') {
console.error('FAIL: reverseString("hello") should be "olleh"');
return false;
}
} catch (error) {
console.log('Function not implemented yet (expected in TDD)');
return false;
}
console.log('✓ reverseString test passed');
return true;
}
// Step 2: Green - Implement function
function reverseString(str) {
return str.split('').reverse().join('');
}
// Step 3: Refactor - Already good, but could optimize
// Current implementation is fine
console.log();
console.log("=== Running All Tests ===");
let allTestsPassed = true;
allTestsPassed = testAdd() && allTestsPassed;
allTestsPassed = testSubtract() && allTestsPassed;
allTestsPassed = testMultiply() && allTestsPassed;
allTestsPassed = testDivide() && allTestsPassed;
allTestsPassed = testCalculateTotal() && allTestsPassed;
allTestsPassed = testValidateEmail() && allTestsPassed;
allTestsPassed = testFindUser() && allTestsPassed;
allTestsPassed = testReverseString() && allTestsPassed;
console.log();
if (allTestsPassed) {
console.log('✓ All tests passed!');
} else {
console.log('✗ Some tests failed');
}
Expected Output:
=== Testing Basics Practice ===
=== Basic Tests ===
Testing add...
✓ add tests passed
Testing subtract...
✓ subtract tests passed
Testing multiply...
✓ multiply tests passed
Testing divide...
✓ divide tests passed
=== Edge Cases ===
Testing calculateTotal...
✓ calculateTotal tests passed
Testing validateEmail...
✓ validateEmail tests passed
Testing findUser...
✓ findUser tests passed
=== TDD Example ===
Testing reverseString (TDD example)...
✓ reverseString test passed
=== Running All Tests ===
✓ All tests passed!
Challenge (Optional):
- Write tests for your own functions
- Practice TDD on a new feature
- Write tests for edge cases
- Build a test suite
Common Mistakes
1. Not Testing Edge Cases
// ❌ Bad: Only test happy path
test('add works', () => {
expect(add(2, 3)).toBe(5);
});
// ✅ Good: Test edge cases
test('add works with positive numbers', () => {
expect(add(2, 3)).toBe(5);
});
test('add works with zero', () => {
expect(add(5, 0)).toBe(5);
});
test('add works with negative numbers', () => {
expect(add(-2, 3)).toBe(1);
});
2. Testing Implementation Details
// ❌ Bad: Test implementation
test('should use reduce', () => {
// Test checks if reduce is used
});
// ✅ Good: Test behavior
test('should calculate total', () => {
expect(calculateTotal(items)).toBe(60);
});
3. Not Cleaning Up
// ❌ Bad: Tests affect each other
let globalData = [];
test('test 1', () => {
globalData.push(1);
});
test('test 2', () => {
// globalData might have data from test 1
});
// ✅ Good: Clean up between tests
beforeEach(() => {
globalData = [];
});
Key Takeaways
- Why Test: Confidence, documentation, refactoring safety
- Test Types: Unit, integration, E2E
- Test Pyramid: Many unit, some integration, few E2E
- TDD: Red-Green-Refactor cycle
- Test Structure: Arrange-Act-Assert
- Edge Cases: Test boundaries and errors
- Best Practice: Test behavior, not implementation
Quiz: Testing Basics
Test your understanding with these questions:
-
Unit testing tests:
- A) Individual functions
- B) Entire application
- C) Both
- D) Neither
-
Integration testing tests:
- A) Single function
- B) Multiple components together
- C) Entire app
- D) Nothing
-
E2E testing tests:
- A) Single function
- B) Components
- C) Entire application
- D) Nothing
-
TDD cycle:
- A) Red-Green-Refactor
- B) Green-Red-Refactor
- C) Refactor-Red-Green
- D) Random
-
Test pyramid has:
- A) Many unit tests
- B) Some integration tests
- C) Few E2E tests
- D) All of the above
-
Tests should:
- A) Test behavior
- B) Test implementation
- C) Both
- D) Neither
-
Edge cases should:
- A) Be tested
- B) Be ignored
- C) Sometimes tested
- D) Never tested
Answers:
- A) Individual functions
- B) Multiple components together
- C) Entire application
- A) Red-Green-Refactor
- D) All of the above
- A) Test behavior
- A) Be tested
Next Steps
Congratulations! You've learned testing basics. You now know:
- Why testing is important
- Different types of testing
- Test-driven development
- How to write basic tests
What's Next?
- Lesson 22.2: Jest Framework
- Learn Jest testing framework
- Write tests with Jest
- Use matchers and mocking
Additional Resources
- Jest Documentation: jestjs.io
- Testing Best Practices: testingjavascript.com
- MDN: Testing: developer.mozilla.org/en-US/docs/Learn/Tools_and_testing
Lesson completed! You're ready to move on to the next lesson.
Course Navigation
- Testing Basics
- Jest Framework
- Advanced Testing