Testing & TDD

Introduction to Software Testing

25 min Lesson 1 of 35

Introduction to Software Testing

Software testing is a systematic process of evaluating and verifying that a software application or system meets specified requirements and functions correctly. Testing is not just about finding bugs—it's about ensuring quality, reliability, and confidence in your code.

Why Test Your Code?

Testing provides numerous benefits that make it an essential part of professional software development:

  • Catch Bugs Early: Finding and fixing bugs during development is significantly cheaper than fixing them in production
  • Confidence in Changes: Tests act as a safety net, allowing you to refactor and add features without fear of breaking existing functionality
  • Documentation: Well-written tests serve as living documentation, showing how your code is intended to be used
  • Better Design: Writing testable code often leads to better architecture and more modular designs
  • Faster Development: While writing tests takes time initially, it speeds up long-term development by catching regressions
  • Team Collaboration: Tests make it easier for team members to understand and modify each other's code
  • Deployment Confidence: Comprehensive tests give you confidence when deploying to production

Important: Testing is an investment. While it requires time upfront, it saves exponentially more time in debugging, maintenance, and preventing production issues.

Types of Software Tests

Software testing encompasses various types of tests, each serving a different purpose:

1. Unit Tests

Test individual components or functions in isolation. These are the most granular tests.

<!-- Example: Testing a simple function --> <script> function add(a, b) { return a + b; } // Unit test test('add should sum two numbers', () => { expect(add(2, 3)).toBe(5); expect(add(-1, 1)).toBe(0); }); </script>

2. Integration Tests

Test how multiple components work together. They verify that different parts of your system integrate correctly.

<!-- Example: Testing database integration --> <?php public function test_user_can_be_saved_to_database() { $user = User::create([ 'name' => 'John Doe', 'email' => 'john@example.com' ]); $this->assertDatabaseHas('users', [ 'email' => 'john@example.com' ]); } ?>

3. Functional Tests

Test complete features or user workflows from an end-user perspective.

4. End-to-End (E2E) Tests

Test the entire application flow from start to finish, simulating real user scenarios.

5. Acceptance Tests

Verify that the system meets business requirements and is acceptable to end users.

6. Performance Tests

Evaluate system performance under various loads and stress conditions.

7. Security Tests

Identify vulnerabilities and ensure the application is secure against threats.

The Testing Pyramid

The testing pyramid is a concept introduced by Mike Cohn that illustrates the ideal distribution of different types of tests:

        /\
       /  \  ← E2E Tests (Few)
      /────\
     / Inte \  ← Integration Tests (Some)
    / gration\
   /──────────\
  /   Unit     \  ← Unit Tests (Many)
 /    Tests     \
/________________\

Why this shape?

  • Unit Tests (Base): Many tests, fast execution, cheap to write and maintain, test small pieces
  • Integration Tests (Middle): Moderate number, slower than unit tests, test component interactions
  • E2E Tests (Top): Few tests, slowest execution, expensive to maintain, test critical user journeys

Best Practice: Follow the 70/20/10 rule: approximately 70% unit tests, 20% integration tests, and 10% end-to-end tests. This provides good coverage while keeping tests fast and maintainable.

Key Testing Terminology

Understanding these terms is essential for effective communication about testing:

Test Case

A specific scenario that tests a particular aspect of functionality. It includes inputs, expected outputs, and preconditions.

Test Suite

A collection of related test cases grouped together.

Assertion

A statement that checks if a condition is true. Tests fail if assertions are false.

<script> // Assertions examples expect(result).toBe(expected); expect(array).toContain(item); expect(value).toBeGreaterThan(10); </script>

Test Coverage

The percentage of your code that is executed by your tests. While 100% coverage is not always necessary, aim for high coverage of critical paths.

Regression Testing

Running tests to ensure that new changes haven't broken existing functionality.

Test Fixture

The fixed state used as a baseline for running tests, ensuring consistency.

Mock/Stub/Spy

Test doubles used to replace real objects in tests:

  • Mock: Simulates object behavior and verifies interactions
  • Stub: Provides predetermined responses to method calls
  • Spy: Records information about how it was called

Setup and Teardown

Code that runs before and after tests to prepare the environment and clean up resources.

<script> describe('User Authentication', () => { beforeEach(() => { // Setup: runs before each test database.clear(); }); afterEach(() => { // Teardown: runs after each test database.disconnect(); }); test('user can login', () => { // Test code here }); }); </script>

Testing Mindset

Effective testing requires the right mindset:

  • Think Like a User: Consider how real users will interact with your code
  • Test Behavior, Not Implementation: Focus on what your code does, not how it does it
  • Expect Failure: Write tests that can actually fail—a test that never fails provides no value
  • Keep Tests Simple: Each test should verify one specific behavior
  • Make Tests Independent: Tests should not depend on each other or run in a specific order
  • Write Descriptive Names: Test names should clearly describe what they verify

Common Mistake: Don't write tests just to achieve high coverage numbers. Focus on testing important behaviors and edge cases. Meaningful tests are better than numerous meaningless tests.

When to Write Tests

There are different approaches to when tests are written:

Test-After Development

Write code first, then write tests. This is common but can lead to code that's hard to test.

Test-Driven Development (TDD)

Write tests before writing the actual code. This approach (covered in lesson 3) ensures your code is testable from the start.

Behavior-Driven Development (BDD)

Similar to TDD but focuses on business behavior and uses more natural language for test descriptions.

Testing Best Practices

  • Start Simple: Begin with the easiest tests and gradually tackle more complex scenarios
  • Test Edge Cases: Don't just test the happy path—test boundary conditions and error cases
  • Keep Tests Fast: Slow tests discourage running them frequently
  • Run Tests Frequently: Run tests every time you make changes
  • One Assertion Per Concept: While multiple assertions are okay, each test should verify one logical concept
  • Use Descriptive Names: Test names should explain what is being tested and what the expected outcome is
  • Avoid Test Interdependence: Each test should be able to run independently

Pro Tip: Follow the FIRST principles for good tests: Fast, Independent, Repeatable, Self-validating, Timely.

Testing ROI (Return on Investment)

While testing requires an initial investment, it provides returns in multiple ways:

  • Reduced debugging time
  • Fewer production bugs
  • Easier refactoring and maintenance
  • Better team productivity
  • Lower support costs
  • Improved customer satisfaction

Exercise

Reflection Questions:

  1. Think about a recent bug you encountered. How could a test have caught it earlier?
  2. What percentage of your current projects have automated tests?
  3. Identify three critical features in your application that would benefit most from testing.
  4. Calculate the time you spent debugging last month. How much of that could have been prevented with tests?

Summary

Software testing is an essential discipline that improves code quality, reduces bugs, and increases development confidence. By understanding the different types of tests, the testing pyramid, and key terminology, you're ready to start writing effective tests. In the next lesson, we'll dive deep into unit testing fundamentals.

Key Takeaways:

  • Testing catches bugs early and provides confidence in your code
  • Different types of tests serve different purposes (unit, integration, E2E)
  • The testing pyramid guides the ideal distribution of test types
  • Good tests are fast, independent, repeatable, self-validating, and timely
  • Testing is an investment that pays off in the long run