What Is Software Testing? Test Types and Best Practices
"We don't have time to write tests." This is one of the most dangerous sentences in a software project. Not writing tests doesn't save time — it borrows future time loss against today.
Why Write Tests?
- Confidence — Know nothing is broken when you change code
- Documentation — Tests document how code should behave
- Refactoring safety — Restructure code safely with tests in place
- Fewer bugs — Catch errors before production
- Speed — Long-term development velocity increases
The Test Pyramid
Mike Cohn's test pyramid shows the ideal distribution of tests:
╱╲
╱ ╲ E2E Tests (few, slow, expensive)
╱────╲
╱ ╲ Integration Tests (moderate)
╱────────╲
╱ ╲ Unit Tests (many, fast, cheap)
╱────────────╲
Unit Tests
Test the smallest code units (functions, methods) in isolation:
function calculateDiscount(price: number, isPremium: boolean): number {
if (isPremium) return price * 0.2;
if (price > 100) return price * 0.1;
return 0;
}
describe('calculateDiscount', () => {
it('should apply 20% for premium users', () => {
expect(calculateDiscount(500, true)).toBe(100);
});
it('should apply 10% for orders over $100', () => {
expect(calculateDiscount(150, false)).toBe(15);
});
it('should not discount normal orders', () => {
expect(calculateDiscount(50, false)).toBe(0);
});
});
Integration Tests
Test how multiple units work together:
describe('Order Flow', () => {
it('should decrease stock when order is created', async () => {
const product = await createProduct({ stock: 10 });
await createOrder({ productId: product.id, quantity: 3 });
const updated = await getProduct(product.id);
expect(updated.stock).toBe(7);
});
});
E2E Tests
Test the application from the user's perspective:
describe('Login Flow', () => {
it('should login with valid credentials', () => {
cy.visit('/login');
cy.get('#email').type('test@example.com');
cy.get('#password').type('password123');
cy.get('#login-btn').click();
cy.url().should('include', '/dashboard');
cy.contains('Welcome').should('be.visible');
});
});
Testing Tools
| Tool | Language/Platform | Type | |------|------------------|------| | Jest | JavaScript/TypeScript | Unit, Integration | | Vitest | Vite-based projects | Unit, Integration | | Cypress | Web | E2E | | Playwright | Web | E2E | | pytest | Python | Unit, Integration | | JUnit | Java | Unit |
Test-Driven Development (TDD)
Write the test first, then the code:
1. 🔴 Red — Write a failing test
2. 🟢 Green — Write minimum code to pass
3. 🔵 Refactor — Clean up, test still passes
4. Repeat
Best Practices
AAA Pattern
Arrange — Set up (prepare test data)
Act — Execute (run the tested code)
Assert — Verify (check the result)
F.I.R.S.T
- Fast — Tests should run quickly
- Independent — Tests should not depend on each other
- Repeatable — Same result every time
- Self-validating — Automatic pass/fail
- Timely — Written alongside the code
Conclusion
Writing tests is not a cost, it's an investment. It takes time initially, but returns tenfold over the project's lifetime. Start by testing critical business logic and frequently changing code.
Learn testing practices with interactive missions at LabLudus.