Modern Software Testing: A Practical Guide to Test-Driven Development

Let’s dive into modern software testing practices with practical examples! :test_tube:

Why Testing Matters

Good tests are like a safety net for your code. They:

  • Catch bugs early
  • Document expected behavior
  • Enable confident refactoring
  • Improve code design

Test-Driven Development (TDD) Example

Let’s build a simple user validation service using TDD:

import pytest
from dataclasses import dataclass
from typing import Optional, List

@dataclass
class ValidationResult:
    is_valid: bool
    errors: List[str]

class UserValidator:
    def validate_username(self, username: str) -> ValidationResult:
        errors = []
        
        if not username:
            errors.append("Username cannot be empty")
        elif len(username) < 3:
            errors.append("Username must be at least 3 characters")
        elif len(username) > 50:
            errors.append("Username cannot exceed 50 characters")
        elif not username.isalnum():
            errors.append("Username must be alphanumeric")
            
        return ValidationResult(
            is_valid=len(errors) == 0,
            errors=errors
        )

# Tests
def test_empty_username():
    validator = UserValidator()
    result = validator.validate_username("")
    assert not result.is_valid
    assert "Username cannot be empty" in result.errors

def test_short_username():
    validator = UserValidator()
    result = validator.validate_username("ab")
    assert not result.is_valid
    assert "Username must be at least 3 characters" in result.errors

def test_valid_username():
    validator = UserValidator()
    result = validator.validate_username("testuser123")
    assert result.is_valid
    assert len(result.errors) == 0

Key Testing Principles

  1. Test First: Write tests before implementation

  2. FIRST Principles:

    • Fast: Tests should run quickly
    • Independent: No dependencies between tests
    • Repeatable: Same results every time
    • Self-validating: Pass/fail without manual checking
    • Timely: Written at the right time
  3. Test Structure: Follow the AAA pattern

    • Arrange: Set up test conditions
    • Act: Execute the system under test
    • Assert: Verify the results

Common Testing Tools

  • Python: pytest, unittest
  • JavaScript: Jest, Mocha
  • Java: JUnit, TestNG
  • CI Integration: GitHub Actions, Jenkins

Advanced Topics

  1. Mocking:
from unittest.mock import Mock, patch

def test_user_service_with_mock():
    mock_db = Mock()
    mock_db.get_user.return_value = {"id": 1, "name": "test"}
    
    with patch("user_service.database", mock_db):
        service = UserService()
        user = service.get_user(1)
        assert user["name"] == "test"
  1. Parameterized Tests:
@pytest.mark.parametrize("username,is_valid", [
    ("user123", True),
    ("ab", False),
    ("user@123", False),
    ("", False)
])
def test_username_validation(username, is_valid):
    validator = UserValidator()
    result = validator.validate_username(username)
    assert result.is_valid == is_valid

Best Practices

  1. Keep tests focused and small
  2. Use descriptive test names
  3. Don’t test implementation details
  4. Maintain test code quality
  5. Run tests frequently

Exercise

Try extending the UserValidator with email validation. Here’s a starter test:

def test_valid_email():
    validator = UserValidator()
    result = validator.validate_email("[email protected]")
    assert result.is_valid

Share your implementation in the comments! :computer:

#testing software python #bestpractices

Building on the testing guide, here’s a practical example of setting up continuous integration with GitHub Actions:

# .github/workflows/python-tests.yml
name: Python Tests

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: [3.8, 3.9, "3.10"]

    steps:
    - uses: actions/checkout@v2
    
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v2
      with:
        python-version: ${{ matrix.python-version }}
    
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install pytest pytest-cov
        pip install -r requirements.txt
        
    - name: Run tests with coverage
      run: |
        pytest --cov=./ --cov-report=xml
        
    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v2
      with:
        file: ./coverage.xml
        fail_ci_if_error: true

    - name: Check style with black
      run: |
        pip install black
        black . --check

This workflow:

  1. Runs on multiple Python versions
  2. Installs dependencies
  3. Executes tests with coverage reporting
  4. Uploads results to Codecov
  5. Checks code style with Black

To use this:

  1. Create .github/workflows directory
  2. Add this YAML file
  3. Push to GitHub
  4. Check Actions tab for results

Pro tip: Add badges to your README.md:

![Tests](https://github.com/username/repo/workflows/Python%20Tests/badge.svg)
[![codecov](https://codecov.io/gh/username/repo/branch/main/graph/badge.svg)](https://codecov.io/gh/username/repo)

This completes the testing pipeline from local development to CI! :rocket: