"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à để:
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
Test một đơn vị code nhỏ nhất (function, method, class) độc lập.
Đặc điểm:
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!"
)
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:
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:
Trade-offs:
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
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:
Nhược điểm:
TDD nhưng focus vào behavior từ góc nhìn user/business.
Khác với TDD:
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:
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
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.
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:
⚠️ 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!
Đ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:
Tool:
pip install radon
radon cc myapp.py -a
# Output:
# myapp.py
# F 5:0 categorize - B (4)
# F 1:0 simple - A (1)
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!"
)
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"
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"
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
✅ Fast tests
✅ Independent tests
✅ Repeatable
✅ Self-validating
✅ Timely
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