Hàm của bạn gọi API thật thì test sẽ chậm, phụ thuộc mạng và có thể tốn tiền. Mocking cho phép thay thành phần bên ngoài bằng một bản giả có hành vi do bạn quy định.
Vấn đề
# thoi_tiet.py
import requests
def nhiet_do(thanh_pho):
r = requests.get(f"https://api.example.com/weather/{thanh_pho}", timeout=10)
return r.json()["temp"]
Mock bằng monkeypatch
# test_thoi_tiet.py
import thoi_tiet
class PhanHoiGia:
def json(self):
return {"temp": 31}
def test_nhiet_do(monkeypatch):
def get_gia(url, timeout=None):
return PhanHoiGia()
monkeypatch.setattr(thoi_tiet.requests, "get", get_gia)
assert thoi_tiet.nhiet_do("hanoi") == 31
monkeypatch tự động khôi phục nguyên trạng sau mỗi test — không rò rỉ sang test khác.
Dùng unittest.mock để kiểm tra cả cách gọi
from unittest.mock import patch, MagicMock
@patch("thoi_tiet.requests.get")
def test_goi_dung_url(mock_get):
mock_get.return_value = MagicMock(**{"json.return_value": {"temp": 25}})
assert thoi_tiet.nhiet_do("hue") == 25
mock_get.assert_called_once()
assert "hue" in mock_get.call_args[0][0]
Quy tắc vàng: vá ở nơi sử dụng
# Đúng: vá tên mà module của bạn nhìn thấy
@patch("thoi_tiet.requests.get")
# Sai: vá tận gốc, module đã import tham chiếu cũ rồi
@patch("requests.get")
Đây là lỗi khiến nhiều người mất hàng giờ. Hãy luôn vá theo đường dẫn trong module đang test.
Giả lập lỗi để test đường thất bại
import pytest, requests
@patch("thoi_tiet.requests.get", side_effect=requests.Timeout)
def test_khi_timeout(mock_get):
with pytest.raises(requests.Timeout):
thoi_tiet.nhiet_do("danang")
Fixture dùng lại nhiều nơi
import pytest
@pytest.fixture
def db_gia():
du_lieu = {}
yield du_lieu
du_lieu.clear() # dọn dẹp sau test
def test_luu(db_gia):
db_gia["k"] = 1
assert db_gia["k"] == 1
Đừng mock quá tay
Mock mọi thứ sẽ cho bạn bộ test luôn xanh nhưng chẳng chứng minh được gì. Nguyên tắc: chỉ mock những gì vượt ra khỏi ranh giới chương trình — mạng, thời gian, hệ thống file, dịch vụ trả phí. Logic của chính bạn thì hãy test thật.
