Phẫu Thuật Mã Nguồn Với Mock Và Patch Trong Python: Tuyệt Chiêu Viết Unit Test Cô Lập

Khi viết kiểm thử tự động (Unit Test), quy tắc vàng là mỗi hàm test phải thực sự cô lập (isolated). Nghĩa là bạn đang kiểm thử logic của hàm hiện tại, chứ không phải đi kiểm thử kết nối mạng, cơ sở dữ liệu bên thứ ba hay các dịch vụ thanh toán ngoài thực tế.
Làm thế nào để giả lập các hành vi phức tạp đó mà không làm hệ thống thật bị ảnh hưởng? Mock và patch trong thư viện chuẩn unittest.mock chính là câu trả lời hoàn hảo. Bài viết này sẽ giúp bạn làm chủ hai công cụ cực kỳ mạnh mẽ này.
🟩 PHẦN 1: TẠO VÀ CẤU HÌNH MOCK (BASIC MOCKING)
1. Import thư viện
Để bắt đầu, bạn cần import các công cụ cần thiết từ module chuẩn của Python:
from unittest.mock import Mock, patch
2. Cấu hình giá trị trả về với return_value
Đối tượng Mock sinh ra để đóng vai giả (diễn viên đóng thế) cho bất kỳ hàm số, đối tượng hay module nào. Bạn có thể định hình trước giá trị trả về của nó bằng thuộc tính return_value:
# Tạo một mock object giả lập API call
mock_api = Mock()
# Cấu hình giá trị trả về cố định
mock_api.get_user.return_value = {"id": 1, "name": "Nguyen Van Bien"}
# Sử dụng thử
print(mock_api.get_user()) # Kết quả: {'id': 1, 'name': 'Nguyen Van Bien'}
3. Sức mạnh linh hoạt của side_effect
Thuộc tính side_effect còn lợi hại hơn return_value rất nhiều vì nó cho phép bạn xử lý hai kịch bản nâng cao:
- Gán một Exception (Ngoại lệ): Dùng để kiểm tra xem hàm của bạn có biết cách bắt lỗi khi dịch vụ bên ngoài bị sập hay không.
- Gán một List (Danh sách): Giúp Mock trả về các giá trị khác nhau ở mỗi lần gọi liên tiếp (Ví dụ: lần 1 trả về dữ liệu mẫu, lần 2 trả về None hoặc lỗi).
# Kịch bản 1: Giả lập gọi API bị lỗi ném ra ConnectionError
mock_service = Mock()
mock_service.connect.side_effect = ConnectionError("Mất kết nối máy chủ!")
# Kịch bản 2: Trả về các giá trị khác nhau qua từng lần gọi
mock_counter = Mock()
mock_counter.get_next_code.side_effect = ["CODE_A", "CODE_B", "CODE_C"]
print(mock_counter.get_next_code()) # In ra: CODE_A
print(mock_counter.get_next_code()) # In ra: CODE_B
4. Giám sát Mock với các hàm Assertions
Không chỉ giả lập dữ liệu, Mock còn ghi nhớ toàn bộ lịch sử cách mà nó được gọi. Bạn có thể dùng các hàm khẳng định để kiểm tra hành vi:
- mock.assert_called(): Kiểm tra xem Mock có từng được gọi ít nhất một lần hay không.
- mock.assert_called_once_with(*args): Kiểm tra xem Mock có được gọi đúng một lần và với chính xác các tham số truyền vào hay không.
def send_welcome_email(email_sender, email: str):
email_sender.send(email, template="welcome")
# Test case
mock_sender = Mock()
send_welcome_email(mock_sender, "bien@example.com")
# Kiểm tra xem hàm send có được gọi đúng tham số không
mock_sender.send.assert_called_once_with("bien@example.com", template="welcome")
🟨 PHẦN 2: SỬ DỤNG PATCH ĐỂ PHẪU THUẬT MÃ NGUỒN (PATCHING)
Khi code của bạn gọi đến một hàm thực tế nằm ở file khác (ví dụ: hàm kết nối database requests.get hoặc hàm ghi file), bạn cần dùng patch để "vá đè" hàm thật đó bằng một Mock trong suốt thời gian chạy test.
1. Cú pháp Decorator (@patch)
Cách phổ biến nhất là đặt decorator @patch lên trên đầu hàm test. Biến Mock tương ứng sẽ được tự động truyền vào làm tham số cuối cùng của hàm test:
import unittest
from unittest.mock import patch
# Giả sử hàm payment_service.process_payment được gọi bên trong code gốc
class TestPayment(unittest.TestCase):
@patch('my_app.payment_service.process_payment')
def test_checkout_success(self, mock_process):
# Cấu hình giả lập kết quả thành công
mock_process.return_value = {"status": "success", "transaction_id": 999}
# Gọi hàm kiểm thử gốc của bạn (Code bên trong sẽ dùng mock_process thay vì code thật)
# result = my_app.checkout(100)
# self.assertTrue(result)
# Đảm bảo hàm thanh toán đã được gọi
mock_process.assert_called_once()
2. Cú pháp Context Manager (with patch)
Nếu bạn không muốn vá đè toàn bộ hàm test mà chỉ muốn thay thế đối tượng trong một vài dòng code cụ thể, hãy dùng khối lệnh with:
def test_send_sms_notification(self):
with patch('my_app.sms_client.send_sms') as mock_sms:
mock_sms.return_value = True
# Chỉ trong khối with này, hàm gửi SMS bị vô hiệu hóa và thay bằng Mock
# response = trigger_notification()
mock_sms.assert_called_once()
3. Nguyên tắc vàng: Định vị đường dẫn chính xác (Where it is used)
⚠️ Lỗi phổ biến nhất của lập trình viên khi dùng patch: Hay điền đường dẫn của nơi định nghĩa hàm thay vì nơi hàm đó đang được sử dụng.
- Sai: patch('requests.get') nếu hàm của bạn import theo kiểu import requests.
- Đúng: Bạn phải patch tại module đang gọi nó (Ví dụ: @patch('my_app.api_client.requests.get')).
- Quy tắc: Hãy patch tại nơi đối tượng được lookup/sử dụng, không phải nơi nó được khai báo.
4. Vá đè cụ thể với patch.object
Nếu bạn không muốn thay thế cả một module mà chỉ muốn vá đè duy nhất một phương thức cụ thể của một thực thể Class (giữ nguyên tất cả các phương thức khác của class đó), hãy dùng patch.object:
class ExternalService:
def fetch_data(self):
return "Real Heavy Data from Server"
def local_calculate(self, x):
return x * 2
def test_local_logic():
service = ExternalService()
# Chỉ vá đè phương thức fetch_data, giữ nguyên local_calculate
with patch.object(service, 'fetch_data', return_value="Mocked Data"):
print(service.fetch_data()) # Kết quả: Mocked Data
print(service.local_calculate(5)) # Kết quả: 10 (Vẫn chạy code thật bình thường!)
📌 Lời Kết
Nắm vững Mock (để giả lập hành vi) và Patch (để cô lập hàm số khỏi các phụ thuộc bên ngoài) sẽ giúp bạn tự tin viết hàng trăm test case chất lượng cao, chạy siêu tốc mà không cần phụ thuộc vào Internet, Database hay các hệ thống ngoài đời thực.
Từ khóa tìm hiểu thêm: Python unittest mock patch tutorial, Python mock side_effect return_value, Where to patch Python mock, patch.object Python example.
Bình luận