Testing Strategy: Xây dựng Lưới An toàn cho Code

"Testing shows the presence, not the absence of bugs." - Edsger W. Dijkstra

Trong thực tế phát triển phần mềm, không có code nào hoàn hảo 100%. Testing không phải để "chứng minh code không có bug", mà là để:

  • Phát hiện bugs sớm (sửa ở development rẻ hơn production gấp 100 lần)
  • Tự tin khi refactor (tests như lưới an toàn)
  • Document behavior (tests là examples sống)
  • Improve design (code khó test thường là code design kém)

Testing Pyramid - Chiến lược Phân bổ Tests

Testing Pyramid của Mike Cohn mô tả tỷ lệ lý tưởng giữa các loại tests.

        /\
       /  \
      /E2E \      ← Ít tests (chậm, dễ vỡ)
     /------\
    /        \
   /Integration\  ← Vừa phải
  /------------\
 /              \
/   Unit Tests   \ ← Nhiều tests nhất (nhanh, stable)
------------------

Tỷ lệ lý tưởng: 70% Unit, 20% Integration, 10% E2E

1. Unit Tests - Nền tảng của Pyramid

Test một đơn vị code nhỏ nhất (function, method, class) độc lập.

Đặc điểm:

  • Fast: Chạy trong milliseconds
  • Isolated: Không phụ thuộc database, network, file system
  • Deterministic: Cùng input → cùng output
  • Focused: Test một logic cụ thể

Ví dụ - Unit test một function:

# calculator.py
def add(a, b):
    return a + b

def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

# test_calculator.py
import pytest

def test_add_positive_numbers():
    assert add(2, 3) == 5

def test_add_negative_numbers():
    assert add(-2, -3) == -5

def test_divide_normal():
    assert divide(10, 2) == 5

def test_divide_by_zero():
    with pytest.raises(ValueError, match="Cannot divide by zero"):
        divide(10, 0)

Best practices cho Unit Tests:

Test cả happy path và edge cases

def test_user_registration():
    # Happy path
    user = register_user("john@example.com", "SecurePass123")
    assert user.email == "john@example.com"
    
    # Edge cases
    with pytest.raises(ValidationError):
        register_user("", "password")  # Empty email
    
    with pytest.raises(ValidationError):
        register_user("john@example.com", "123")  # Weak password

Dùng descriptive test names

# ❌ BAD
def test1(): pass
def test_user(): pass

# ✅ GOOD - Nói rõ behavior
def test_user_registration_with_valid_credentials_succeeds(): pass
def test_user_registration_with_existing_email_raises_error(): pass

Follow AAA pattern: Arrange, Act, Assert

def test_order_total_calculation():
    # Arrange - Setup test data
    order = Order()
    order.add_item(Product("Laptop", 1000), quantity=2)
    order.add_item(Product("Mouse", 25), quantity=1)
    
    # Act - Execute the behavior
    total = order.calculate_total()
    
    # Assert - Verify result
    assert total == 2025

Mock external dependencies

from unittest.mock import Mock, patch

def send_welcome_email(user):
    email_service = EmailService()
    email_service.send(user.email, "Welcome!", "Hello!")

def test_send_welcome_email():
    # Mock EmailService để không gửi email thật
    with patch('myapp.EmailService') as MockEmailService:
        mock_service = MockEmailService.return_value
        
        user = User(email="john@example.com")
        send_welcome_email(user)
        
        # Verify email service được gọi đúng
        mock_service.send.assert_called_once_with(
            "john@example.com",
            "Welcome!",
            "Hello!"
        )

2. Integration Tests - Test sự Tương tác

Test nhiều components/modules làm việc cùng nhau.

Ví dụ - Test API endpoint với database:

import pytest
from fastapi.testclient import TestClient
from myapp import app, database

@pytest.fixture
def client():
    # Setup: Create test database
    database.create_tables()
    client = TestClient(app)
    yield client
    # Teardown: Clean up
    database.drop_tables()

def test_create_user_endpoint(client):
    # Arrange
    user_data = {
        "name": "John Doe",
        "email": "john@example.com"
    }
    
    # Act - Gọi API thật
    response = client.post("/users", json=user_data)
    
    # Assert
    assert response.status_code == 201
    assert response.json()["email"] == "john@example.com"
    
    # Verify data actually saved in database
    saved_user = database.get_user_by_email("john@example.com")
    assert saved_user is not None
    assert saved_user.name == "John Doe"

Integration test với external services:

def test_payment_processing_integration():
    # Sử dụng test/sandbox environment của payment gateway
    payment_gateway = StripeGateway(api_key=TEST_API_KEY)
    
    order = Order(total=100)
    result = payment_gateway.charge(order, test_card_number="4242424242424242")
    
    assert result.success is True
    assert result.transaction_id is not None

Trade-offs:

  • Chậm hơn unit tests (có I/O operations)
  • Phức tạp hơn để setup (cần test database, test accounts)
  • Nhưng phát hiện được integration issues mà unit tests bỏ sót

3. System Tests (E2E) - Test Toàn bộ Hệ thống

Test user workflows từ đầu đến cuối qua UI hoặc API.

Ví dụ - E2E test với Selenium:

from selenium import webdriver
from selenium.webdriver.common.by import By

def test_user_checkout_flow():
    driver = webdriver.Chrome()
    
    try:
        # 1. User đăng nhập
        driver.get("https://example.com/login")
        driver.find_element(By.ID, "email").send_keys("john@example.com")
        driver.find_element(By.ID, "password").send_keys("password123")
        driver.find_element(By.ID, "login-btn").click()
        
        # 2. User thêm sản phẩm vào cart
        driver.get("https://example.com/products/laptop")
        driver.find_element(By.ID, "add-to-cart").click()
        
        # 3. User checkout
        driver.get("https://example.com/cart")
        driver.find_element(By.ID, "checkout-btn").click()
        
        # 4. User điền thông tin và thanh toán
        driver.find_element(By.ID, "card-number").send_keys("4242424242424242")
        driver.find_element(By.ID, "submit-payment").click()
        
        # 5. Verify order success
        success_message = driver.find_element(By.CLASS_NAME, "success").text
        assert "Order confirmed" in success_message
    finally:
        driver.quit()

E2E với API (không qua UI):

def test_order_creation_workflow():
    # 1. Register user
    register_response = requests.post(
        f"{API_URL}/register",
        json={"email": "test@example.com", "password": "password"}
    )
    assert register_response.status_code == 201
    
    # 2. Login để lấy token
    login_response = requests.post(
        f"{API_URL}/login",
        json={"email": "test@example.com", "password": "password"}
    )
    token = login_response.json()["token"]
    
    # 3. Create order
    order_response = requests.post(
        f"{API_URL}/orders",
        headers={"Authorization": f"Bearer {token}"},
        json={"items": [{"product_id": 1, "quantity": 2}]}
    )
    assert order_response.status_code == 201
    order_id = order_response.json()["id"]
    
    # 4. Verify order status
    status_response = requests.get(
        f"{API_URL}/orders/{order_id}",
        headers={"Authorization": f"Bearer {token}"}
    )
    assert status_response.json()["status"] == "pending"

Khi nào dùng E2E:

  • Critical user journeys (checkout, payment, signup)
  • Cross-browser compatibility
  • Performance testing

Trade-offs:

  • Rất chậm (seconds/minutes per test)
  • Flaky (dễ fail vì network, timing issues)
  • Expensive để maintain

4. Acceptance Tests (UAT)

Verify hệ thống đáp ứng business requirements.

Ví dụ - Gherkin syntax (BDD style):

Feature: User Registration
  As a new user
  I want to register an account
  So that I can access the platform

  Scenario: Successful registration with valid credentials
    Given I am on the registration page
    When I enter email "john@example.com"
    And I enter password "SecurePass123"
    And I click "Register"
    Then I should see "Registration successful"
    And I should receive a welcome email

  Scenario: Registration fails with existing email
    Given a user with email "existing@example.com" already exists
    When I try to register with email "existing@example.com"
    Then I should see error "Email already registered"

Implementation với Behave (Python):

# steps/registration.py
from behave import given, when, then

@given('I am on the registration page')
def step_impl(context):
    context.browser.get("https://example.com/register")

@when('I enter email "{email}"')
def step_impl(context, email):
    context.browser.find_element(By.ID, "email").send_keys(email)

@then('I should see "{message}"')
def step_impl(context, message):
    assert message in context.browser.page_source

Testing Methodologies

Test Driven Development (TDD)

Red-Green-Refactor cycle:

1. RED: Viết test trước (test fail vì chưa có code)
2. GREEN: Viết code tối thiểu để pass test
3. REFACTOR: Improve code mà vẫn pass test
4. Repeat

Ví dụ TDD workflow:

# Step 1: RED - Viết test trước
def test_calculate_discount():
    assert calculate_discount(100, 0.1) == 10

# → Chạy test → FAIL (function chưa tồn tại)

# Step 2: GREEN - Code tối thiểu
def calculate_discount(price, rate):
    return price * rate

# → Chạy test → PASS

# Step 3: REFACTOR - Improve code
def calculate_discount(price: float, rate: float) -> float:
    """Calculate discount amount.
    
    Args:
        price: Original price
        rate: Discount rate (0.0 to 1.0)
    
    Returns:
        Discount amount
    """
    if not 0 <= rate <= 1:
        raise ValueError("Rate must be between 0 and 1")
    
    return round(price * rate, 2)

# → Chạy test → Vẫn PASS

# Step 4: Add more tests
def test_calculate_discount_with_invalid_rate():
    with pytest.raises(ValueError):
        calculate_discount(100, 1.5)

Lợi ích của TDD:

  • Code được designed để testable từ đầu
  • High test coverage tự nhiên
  • Ít bugs hơn
  • Documentation qua tests

Nhược điểm:

  • Chậm hơn ban đầu
  • Cần discipline
  • Có thể over-test trivial code

Behavior Driven Development (BDD)

TDD nhưng focus vào behavior từ góc nhìn user/business.

Khác với TDD:

  • TDD: Developer-centric, technical language
  • BDD: Business-centric, natural language (Gherkin)

Example so sánh:

# TDD style
def test_order_total_with_tax():
    order = Order()
    order.add_item(100)
    assert order.get_total_with_tax(0.1) == 110

# BDD style (Gherkin)
Scenario: Calculate order total with tax
  Given an order with subtotal of $100
  When tax rate is 10%
  Then the total should be $110

Khi nào dùng BDD:

  • Requirements phức tạp cần collaboration với non-technical stakeholders
  • User stories là source of truth
  • Cần living documentation

Black-box vs White-box Testing

Black-box Testing

Test behavior mà không biết internal implementation.

# Tester không biết code bên trong
def test_login():
    # Input: valid credentials
    result = login("user@example.com", "password123")
    # Expected output: success
    assert result.success is True

# Tester chỉ biết: đúng credentials → login success

Pros: Test từ user perspective, không bị bias bởi code
Cons: Khó achieve full code coverage

White-box Testing

Test với knowledge về internal structure.

# Tester biết code có edge case với empty password
def test_login_with_empty_password():
    # Test specifically cho branch: if password == ""
    result = login("user@example.com", "")
    assert result.error == "Password required"

# Tester biết SQL injection vulnerability
def test_login_sql_injection_prevention():
    result = login("admin' OR '1'='1", "anything")
    assert result.success is False

Pros: Thorough coverage, test edge cases
Cons: Tests coupled với implementation (dễ vỡ khi refactor)

Best practice: Combine cả hai - black-box cho behavior, white-box cho coverage.

Test Coverage Metrics

Code Coverage

Phần trăm code được execute bởi tests.

# Python - pytest-cov
pip install pytest-cov
pytest --cov=myapp --cov-report=html

# Output:
# Name                Stmts   Miss  Cover
# ---------------------------------------
# myapp/auth.py          50      5    90%
# myapp/database.py      80     20    75%
# ---------------------------------------
# TOTAL                 130     25    81%

Loại coverage:

1. Line Coverage: Bao nhiêu lines được execute?

def divide(a, b):
    if b == 0:         # Line 1
        return None    # Line 2
    return a / b       # Line 3

# Test chỉ gọi divide(10, 2) → Line coverage 66% (Line 2 không chạy)

2. Branch Coverage: Bao nhiêu branches (if/else) được test?

# Để 100% branch coverage, cần test cả:
divide(10, 2)   # b != 0 branch
divide(10, 0)   # b == 0 branch

3. Function Coverage: Bao nhiêu functions được call?

Mục tiêu coverage:

  • Minimum: 70%
  • Good: 80-90%
  • Excellent: 90%+

⚠️ Lưu ý: 100% coverage ≠ bug-free! Coverage chỉ là một metric.

# 100% coverage nhưng vẫn có bug
def add(a, b):
    return a - b  # BUG: Should be +

def test_add():
    result = add(2, 3)
    assert result  # Pass (result = -1, truthy) nhưng sai logic!

Cyclomatic Complexity

Đo độ phức tạp của code - số lượng independent paths.

# Complexity = 1 (no branches)
def simple():
    return True

# Complexity = 2 (1 if)
def check_age(age):
    if age >= 18:
        return "Adult"
    return "Minor"

# Complexity = 4 (3 ifs)
def categorize(age):
    if age < 13:
        return "Child"
    elif age < 18:
        return "Teen"
    elif age < 65:
        return "Adult"
    else:
        return "Senior"

Guidelines:

  • 1-10: Simple, easy to test
  • 11-20: Moderate complexity
  • 21+: High complexity - consider refactoring

Tool:

pip install radon
radon cc myapp.py -a

# Output:
# myapp.py
#     F 5:0 categorize - B (4)
#     F 1:0 simple - A (1)

Test Doubles - Isolate Dependencies

1. Mock - Verify interactions

from unittest.mock import Mock

def test_send_notification():
    # Create mock
    email_service = Mock()
    
    # Execute code
    notify_user("john@example.com", "Welcome!")
    
    # Verify mock được call đúng
    email_service.send.assert_called_once_with(
        to="john@example.com",
        subject="Welcome!"
    )

2. Stub - Provide canned responses

from unittest.mock import Mock

def test_get_user_info():
    # Stub database
    db = Mock()
    db.get_user.return_value = {"id": 1, "name": "John"}
    
    user_service = UserService(db)
    user = user_service.get_user_info(1)
    
    assert user["name"] == "John"

3. Fake - Working implementation

class FakeDatabase:
    """In-memory database cho testing"""
    def __init__(self):
        self.data = {}
    
    def save(self, key, value):
        self.data[key] = value
    
    def get(self, key):
        return self.data.get(key)

def test_user_repository():
    # Dùng fake thay vì real database
    db = FakeDatabase()
    repo = UserRepository(db)
    
    user = User(id=1, name="John")
    repo.save(user)
    
    retrieved = repo.get(1)
    assert retrieved.name == "John"

4. Spy - Record calls

class SpyEmailService:
    def __init__(self):
        self.calls = []
    
    def send(self, to, subject, body):
        self.calls.append({"to": to, "subject": subject})

def test_notification_count():
    spy = SpyEmailService()
    
    notify_users(["user1@example.com", "user2@example.com"])
    
    # Verify number of calls
    assert len(spy.calls) == 2

Best Practices

Fast tests

  • Unit tests < 100ms
  • Integration tests < 5s
  • Toàn bộ suite < 10 minutes

Independent tests

  • Mỗi test có thể chạy riêng
  • Không phụ thuộc order
  • Clean up sau mỗi test

Repeatable

  • Cùng input → cùng output
  • Không phụ thuộc external state (current time, random)

Self-validating

  • Test tự check kết quả (assert)
  • Pass/Fail rõ ràng

Timely

  • Viết test sớm (TDD) hoặc ngay sau khi viết code
  • Đừng để đống code chưa test tích lũy

Key Takeaways

  • Testing Pyramid: 70% Unit, 20% Integration, 10% E2E
  • Unit tests nhanh, isolated, focused - nền tảng của testing strategy
  • Integration tests verify components làm việc cùng nhau
  • E2E tests test critical user journeys - ít nhưng quan trọng
  • TDD (Red-Green-Refactor) giúp design testable code
  • BDD focus vào behavior với natural language
  • Coverage là metric hữu ích nhưng không phải mục tiêu duy nhất
  • Test doubles (Mock, Stub, Fake, Spy) isolate dependencies

Trong bài tiếp theo, chúng ta sẽ tìm hiểu Version Control & CI/CD - cách làm việc với Git, branching strategies, và automated deployment pipelines.


Bài viết thuộc series "From Zero to AI Engineer" - Module 3: Implementation & Quality Assurance