「系統上線後,用戶抱怨:『為什麼載入要等這麼久?』」這是每個開發者的惡夢。今天,我們將學習如何在開發階段就發現並解決效能問題,透過 TDD 的方式確保程式碼不僅正確,還要夠快!
基礎測試 ✅ → 實戰專案 ✅ → 框架測試 ✅ → 【效能測試 📍】→ 最後階段
經過 25 天的學習,我們已經掌握了功能測試的精髓。今天要進入效能測試的世界,學習如何量化程式碼的執行速度,並在 TDD 過程中確保效能符合預期。
# 傳統測試:只確保功能正確
def test_search_returns_results():
results = search("python")
assert len(results) > 0
# 效能測試:還要確保速度夠快
def test_search_performance():
import time
start = time.time()
results = search("python")
duration = time.time() - start
assert duration < 1.0 # 必須在 1 秒內完成
最簡單的計時方法:
# 建立 tests/day26/test_basic_timing.py
import time
import pytest
def slow_function():
"""模擬耗時操作"""
time.sleep(0.1)
return "done"
def test_function_speed():
start = time.perf_counter()
result = slow_function()
duration = time.perf_counter() - start
assert result == "done"
assert duration < 0.2 # 容許誤差
專業的效能測試框架:
# 安裝
pip install pytest-benchmark
# 建立 tests/day26/test_benchmark.py
import pytest
def fibonacci(n):
"""計算費氏數列"""
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
def test_fibonacci_benchmark(benchmark):
# benchmark 會自動執行多次並統計
result = benchmark(fibonacci, 20)
assert result == 6765
執行測試會看到詳細報告:
------------------------- benchmark: 1 tests -------------------------
Name Min Max Mean StdDev Median
test_fibonacci_benchmark 1.2345 1.5678 1.3456 0.0234 1.3400
# 建立 tests/day26/test_todo_performance.py
import pytest
import time
from typing import List
from dataclasses import dataclass
@dataclass(frozen=True) # frozen=True 使 Todo 可以被 hash
class Todo:
id: int
title: str
completed: bool = False
class TodoService:
def __init__(self):
self.todos: List[Todo] = []
def search_todos(self, keyword: str) -> List[Todo]:
"""簡單的線性搜尋"""
results = []
for todo in self.todos:
if keyword.lower() in todo.title.lower():
results.append(todo)
return results
def test_search_performance_baseline():
"""測試:搜尋 1000 筆資料應在 0.1 秒內完成"""
service = TodoService()
# 準備測試資料
for i in range(1000):
service.todos.append(
Todo(id=i, title=f"Task {i % 100}")
)
# 執行搜尋並計時
start = time.perf_counter()
results = service.search_todos("Task 50")
duration = time.perf_counter() - start
assert len(results) == 10 # 應找到 10 筆
assert duration < 0.1 # 應在 0.1 秒內完成
# 更新 tests/day26/test_todo_performance.py
def test_search_performance_large_dataset():
"""測試:搜尋 10000 筆資料的效能"""
service = TodoService()
# 準備大量測試資料
for i in range(10000):
service.todos.append(
Todo(id=i, title=f"Task {i % 100}")
)
start = time.perf_counter()
results = service.search_todos("Task 99")
duration = time.perf_counter() - start
print(f"搜尋 10000 筆耗時:{duration:.3f} 秒")
assert duration < 1.0 # 可能會失敗!
# 建立 tests/day26/test_todo_performance.py
from collections import defaultdict
class OptimizedTodoService:
def __init__(self):
self.todos: List[Todo] = []
self.title_index = defaultdict(list) # 標題索引
def add_todo(self, todo: Todo):
self.todos.append(todo)
# 建立索引
words = todo.title.lower().split()
for word in words:
self.title_index[word].append(todo)
def search_todos(self, keyword: str) -> List[Todo]:
"""使用索引加速搜尋"""
keyword = keyword.lower()
results = set()
# 從索引中查找
for word, todos in self.title_index.items():
if keyword in word:
results.update(todos)
return list(results)
def test_optimized_search_performance():
"""測試:優化後的搜尋效能"""
service = OptimizedTodoService()
# 準備測試資料
for i in range(10000):
todo = Todo(id=i, title=f"Task {i % 100}")
service.add_todo(todo)
start = time.perf_counter()
results = service.search_todos("Task")
duration = time.perf_counter() - start
print(f"優化後搜尋耗時:{duration:.3f} 秒")
assert duration < 0.5 # 效能提升!
# 建立 tests/day26/test_benchmark_comparison.py
import pytest
from typing import List
def linear_search(items: List[int], target: int) -> int:
"""線性搜尋"""
for i, item in enumerate(items):
if item == target:
return i
return -1
def binary_search(items: List[int], target: int) -> int:
"""二分搜尋(需要排序)"""
left, right = 0, len(items) - 1
while left <= right:
mid = (left + right) // 2
if items[mid] == target:
return mid
elif items[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
@pytest.fixture
def test_data():
return list(range(10000))
def test_linear_search_benchmark(benchmark, test_data):
result = benchmark(linear_search, test_data, 9999)
assert result == 9999
def test_binary_search_benchmark(benchmark, test_data):
result = benchmark(binary_search, test_data, 9999)
assert result == 9999
# 建立 tests/day26/test_memory_performance.py
import tracemalloc
def test_memory_usage():
"""測試記憶體使用量"""
tracemalloc.start()
# 執行操作
large_list = [i for i in range(1_000_000)]
current, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
peak_mb = peak / 1024 / 1024
print(f"尖峰記憶體:{peak_mb:.2f} MB")
assert peak_mb < 100 # 不超過 100 MB
# 建立 tests/day26/test_async_performance.py
import pytest
import asyncio
import time
async def fetch_sequential():
"""循序執行三次 0.1 秒操作"""
for _ in range(3):
await asyncio.sleep(0.1)
async def fetch_concurrent():
"""並行執行三次 0.1 秒操作"""
await asyncio.gather(*[asyncio.sleep(0.1) for _ in range(3)])
@pytest.mark.asyncio
async def test_concurrent_faster():
"""測試並行比循序快"""
# 循序:約 0.3 秒
start = time.perf_counter()
await fetch_sequential()
seq_time = time.perf_counter() - start
# 並行:約 0.1 秒
start = time.perf_counter()
await fetch_concurrent()
con_time = time.perf_counter() - start
assert con_time < seq_time * 0.5 # 至少快 2 倍
# ❌ 錯誤:多次查詢
for todo in todos:
todo.tags = get_tags_by_id(todo.id) # N 次
# ✅ 正確:批次查詢
tags = get_tags_by_ids([t.id for t in todos]) # 1 次
def test_reasonable_performance():
"""根據需求設定合理期望"""
# ❌ 太嚴格
assert response_time < 0.001
# ✅ 合理的期望
assert response_time < 1.0 # 一般 API
assert response_time < 0.1 # 快取查詢
@pytest.fixture
def isolated_service():
"""確保每次測試都是乾淨的環境"""
service = TodoService()
yield service
service.clear() # 清理
def test_statistical_performance(benchmark):
"""使用統計確保穩定性"""
results = benchmark.stats
assert results['median'] < 0.1
assert results['stddev'] < 0.01 # 變異要小
# 完整實作 tests/day26/test_performance_monitor.py
import time
import statistics
from typing import Callable
def measure_performance(func: Callable, *args, runs: int = 10) -> dict:
"""測量函數效能"""
times = []
for _ in range(runs):
start = time.perf_counter()
func(*args)
times.append(time.perf_counter() - start)
return {
'min': min(times),
'max': max(times),
'mean': statistics.mean(times),
'median': statistics.median(times),
'stddev': statistics.stdev(times) if len(times) > 1 else 0
}
def test_performance_monitor():
"""測試效能監控系統"""
def sample_function(n: int):
return sum(range(n))
# 測量效能
result = measure_performance(sample_function, 10000, runs=20)
# 驗證結果
assert result['median'] < 0.01
assert result['stddev'] < result['mean'] * 0.1
print(f"效能:中位數 {result['median']:.4f}s")
試試看這些進階效能測試:
為搜尋功能加入快取,測試效能提升:
# 挑戰:加入 @lru_cache 裝飾器
from functools import lru_cache
@lru_cache(maxsize=128)
def expensive_search(keyword: str) -> List:
# 你的實作...
pass
def test_cache_performance():
# 第一次:慢
start = time.perf_counter()
result1 = expensive_search("test")
time1 = time.perf_counter() - start
# 第二次:快(從快取)
start = time.perf_counter()
result2 = expensive_search("test")
time2 = time.perf_counter() - start
assert time2 < time1 * 0.1
今天我們學習了效能測試的核心技術:
效能測試不是錦上添花,而是確保系統品質的必要環節。透過 TDD 的方式,我們可以在開發初期就建立效能基準,避免後期的效能災難。
明天是 Day 27,我們將進入測試的最後階段,完成 TDD 旅程的最後一塊拼圖!
嘗試為你的專案加入效能測試:
記住:「快」不是偶然,是刻意設計和持續測試的結果!