Testing Fundamentals
Learn the universal principles of software testing that apply across all languages and frameworks. These fundamentals form the foundation for writing reliable, maintainable tests.
What You'll Learn
- Testing Basics - Why test, types of tests, and testing pyramid
- Testing Types - Unit, integration, E2E testing
- Test-Driven Development (TDD) - Write tests first
- Behavior-Driven Development (BDD) - Describe behavior in plain language
Why Testing Matters
For Developers
- Confidence - Know your code works as expected
- Refactoring Safety - Change code without breaking functionality
- Documentation - Tests document how code should behave
- Bug Prevention - Catch issues before production
For Teams
- Collaboration - Tests clarify requirements
- Code Review - Tests show intent and edge cases
- Onboarding - New developers learn from tests
- Quality - Maintain standards across the codebase
For Business
- Cost Savings - Bugs caught early cost less to fix
- Faster Delivery - Confidence enables rapid deployment
- Customer Satisfaction - Fewer bugs in production
- Compliance - Meet quality standards and regulations
The Testing Pyramid
/\
/E2E\ Few tests (slow, brittle, expensive)
/______\
/ \
/Integration\ Some tests (moderate speed and cost)
/__________ \
/ \
/ Unit Tests \ Many tests (fast, cheap, reliable)
/__________________\
Unit Tests (70%)
Test individual functions or methods in isolation.
Characteristics:
- Very fast (milliseconds)
- No external dependencies
- Test one thing at a time
- Easy to maintain
Example:
function add(a, b) {
return a + b;
}
test('add should sum two numbers', () => {
expect(add(2, 3)).toBe(5);
});
Integration Tests (20%)
Test how multiple components work together.
Characteristics:
- Moderate speed (seconds)
- May use real dependencies (databases, APIs)
- Test component interactions
- More complex setup
Example:
test('user service should save to database', async () => {
const user = await userService.create({ name: 'Alice' });
const saved = await database.findById(user.id);
expect(saved.name).toBe('Alice');
});
End-to-End Tests (10%)
Test the entire application from user perspective.
Characteristics:
- Slow (minutes)
- Tests real user workflows
- Use real browsers
- Brittle and expensive
Example:
test('user can login and view dashboard', async () => {
await page.goto('http://localhost:3000');
await page.fill('#email', 'user@example.com');
await page.fill('#password', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
});
Test Types Explained
Unit Tests
Focus on individual units of code (functions, methods, classes).
When to use:
- Testing business logic
- Testing pure functions
- Testing utility functions
- Testing algorithms
Integration Tests
Test how components work together.
When to use:
- Testing API endpoints
- Testing database operations
- Testing service interactions
- Testing third-party integrations
End-to-End Tests
Test complete user workflows.
When to use:
- Critical user paths (login, checkout)
- Cross-browser compatibility
- Visual regression testing
- Smoke tests for deployments
Other Test Types
Acceptance Tests:
- Verify software meets requirements
- Often written in BDD style
- Stakeholder-readable
Performance Tests:
- Load testing
- Stress testing
- Benchmark testing
Security Tests:
- Penetration testing
- Vulnerability scanning
- Authentication testing
Best Practices
1. Follow AAA Pattern
Arrange, Act, Assert - Structure every test clearly.
test('user can be created', () => {
// Arrange - Set up test data
const userData = { name: 'Alice', email: 'alice@example.com' };
// Act - Perform the action
const user = createUser(userData);
// Assert - Verify the result
expect(user.name).toBe('Alice');
expect(user.email).toBe('alice@example.com');
});
2. Test Behavior, Not Implementation
// ❌ Bad - Testing implementation details
test('add function uses + operator', () => {
const source = add.toString();
expect(source).toContain('+');
});
// ✅ Good - Testing behavior
test('add returns sum of two numbers', () => {
expect(add(2, 3)).toBe(5);
});
3. One Assert Per Test (When Possible)
// ❌ Bad - Testing multiple things
test('user creation', () => {
const user = createUser({ name: 'Alice' });
expect(user.name).toBe('Alice');
expect(user.id).toBeDefined();
expect(user.createdAt).toBeInstanceOf(Date);
expect(user.isActive).toBe(true);
});
// ✅ Good - Separate tests for separate concerns
test('user should have provided name', () => {
const user = createUser({ name: 'Alice' });
expect(user.name).toBe('Alice');
});
test('user should have generated id', () => {
const user = createUser({ name: 'Alice' });
expect(user.id).toBeDefined();
});
4. Use Descriptive Test Names
// ❌ Bad
test('test1', () => { ... });
test('it works', () => { ... });
// ✅ Good
test('should return sum of two positive numbers', () => { ... });
test('should throw error when dividing by zero', () => { ... });
test('should create user with valid email', () => { ... });
5. Test Edge Cases
test('add should handle positive numbers', () => {
expect(add(2, 3)).toBe(5);
});
test('add should handle negative numbers', () => {
expect(add(-2, -3)).toBe(-5);
});
test('add should handle zero', () => {
expect(add(0, 5)).toBe(5);
});
test('add should handle decimals', () => {
expect(add(0.1, 0.2)).toBeCloseTo(0.3);
});
Common Testing Mistakes
1. Not Testing Edge Cases
Only testing happy paths leaves bugs undiscovered.
2. Testing Too Much Implementation
Tests break when refactoring, even though behavior hasn't changed.
3. Slow Tests
Tests that take minutes to run won't be run frequently.
4. Flaky Tests
Tests that randomly fail reduce confidence and waste time.
5. No Test Isolation
Tests that depend on each other create cascading failures.
6. Poor Test Names
Vague names make it hard to understand what broke.
Language-Specific Testing
After mastering these fundamentals, explore language-specific testing:
- Java Testing - JUnit 5, Mockito
- JavaScript Testing - Jest, Mocha
- TypeScript Testing - Jest with TypeScript
- React Testing - React Testing Library
- Angular Testing - Jasmine, Karma
Testing Practices
Test-Driven Development (TDD)
Write tests before code.
Benefits:
- Better design
- Higher test coverage
- Fewer bugs
Test Doubles
Mocks, stubs, spies, and fakes for isolating tests.
Learn:
- When to use each type
- Mocking frameworks
- Best practices
Resources
- Testing JavaScript - Kent C. Dodds
- Test Driven Development - Kent Beck
- Growing Object-Oriented Software, Guided by Tests
- The Practical Test Pyramid - Martin Fowler
Next Steps
- Read Testing Basics for detailed fundamentals
- Explore Testing Types to understand unit, integration, and E2E tests
- Learn Test-Driven Development (TDD) methodology
- Discover Behavior-Driven Development (BDD) for stakeholder collaboration
- Master Test Doubles (mocks, stubs, spies)
- Choose your language/framework and apply these concepts
Good tests are an investment in your codebase. Start testing today!