Cú điện thoại lúc 2 giờ sáng và bài học về Unit Test
Tôi vẫn nhớ như in đêm trực hệ thống ba năm trước. Đúng 2 giờ sáng, màn hình Slack rung liên hồi vì CI/CD pipeline báo đỏ rực. Cả team nháo nhào kiểm tra nhưng code logic hoàn toàn bình thường. Nguyên nhân hóa ra cực kỳ hy hữu: API của bên đối tác đang bảo trì định kỳ. Thậm chí, có lần test fail chỉ vì server database dùng chung bị ai đó lỡ tay “drop table”.
Đó là lúc tôi nhận ra một sự thật phũ phàng. Nếu Unit Test vẫn cần Internet để chạy, bạn đang làm Integration Test (kiểm thử tích hợp) mà không biết. Một Unit Test đúng nghĩa phải cô lập hoàn toàn logic khỏi các yếu tố bên ngoài. Để giải quyết triệt để sự phụ thuộc này, unittest.mock là công cụ bắt buộc phải có trong bộ kỹ năng của Python developer.
Tại sao bạn nên ngừng dùng kết nối thật khi test?
Hãy nhìn vào ba cách tiếp cận phổ biến khi xử lý code có gọi API hoặc Database:
- Kết nối thật (Real Connection): Bạn dựng database test hoặc gọi API thật. Cách này chính xác nhưng cực kỳ chậm. Một bộ test suite có thể mất 15 phút để chạy thay vì 30 giây, chưa kể rủi ro mạng chập chờn.
- Tạo Fake Object thủ công: Bạn tự viết class giả lập. Cách này tốn công bảo trì và làm file test phình to nhanh chóng.
- Sử dụng unittest.mock: Bạn thay thế các phần phụ thuộc bằng đối tượng “giả”. Bạn có thể ép nó trả về kết quả bất kỳ hoặc giả lập lỗi server chỉ với 2 dòng code.
Điểm cộng lớn nhất của unittest.mock là nó có sẵn trong thư viện chuẩn của Python (từ bản 3.3). Bạn không cần cài thêm thư viện ngoài. Nó cực kỳ linh hoạt, cho phép bạn “tráo” một hàm, một class hay thậm chí là thuộc tính của object ngay khi test đang chạy.
Cách giả lập API để test nhanh hơn
Giả sử bạn có một hàm lấy giá vàng từ API. Nếu website đó sập, test của bạn sẽ hỏng theo. Đây là cách chúng ta cô lập nó:
import requests
def get_gold_price(api_url):
response = requests.get(api_url)
if response.status_code == 200:
return response.json()["price"]
return None
Thay vì gọi requests.get thật, chúng ta dùng decorator @patch để kiểm soát kết quả trả về:
import unittest
from unittest.mock import patch
class TestGoldAPI(unittest.TestCase):
@patch('requests.get')
def test_get_gold_price_success(self, mock_get):
# Giả lập phản hồi từ server
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {"price": 2000}
result = get_gold_price("http://fake-api.com")
self.assertEqual(result, 2000)
# Xác nhận hàm được gọi đúng tham số
mock_get.assert_called_once_with("http://fake-api.com")
Giả lập Database: Tiết kiệm hàng giờ chờ đợi
Thao tác với Database trong Unit Test thường là một “cơn ác mộng” về hiệu năng. Thay vì chờ DB thực thi câu lệnh SQL mất vài trăm miligiây, Mock giúp bạn nhận kết quả ngay lập tức trong 1-2 miligiây. Dưới đây là cách mock một Database session với SQLAlchemy:
from unittest.mock import MagicMock
def test_delete_user_exists():
# Tạo session giả
mock_session = MagicMock()
# Giả lập chuỗi lệnh query.filter_by.first
mock_user = MagicMock()
mock_session.query.return_value.filter_by.return_value.first.return_value = mock_user
result = delete_user(mock_session, 123)
assert result is True
mock_session.delete.assert_called_once_with(mock_user)
mock_session.commit.assert_called_once()
Thực tế, khi xử lý dữ liệu đầu vào trước khi lưu DB, tôi thường xuyên vấp phải lỗi Regex. Để tiết kiệm thời gian debug trong code test, tôi hay dùng công cụ kiểm tra Regex để test pattern trước khi đưa vào Python. Việc này giúp giảm thiểu đáng kể số lần phải chạy lại Unit Test chỉ vì sai một dấu gạch chéo.
Xử lý tình huống lỗi với side_effect
Code không phải lúc nào cũng chạy suôn sẻ. Một bộ test chất lượng cần kiểm tra xem ứng dụng sẽ làm gì nếu API bị timeout hoặc database mất kết nối. Thuộc tính side_effect cho phép bạn ném ra một Exception để kiểm tra logic xử lý lỗi.
@patch('requests.get')
def test_get_gold_price_timeout(self, mock_get):
# Ép hàm requests.get ném ra lỗi Timeout
mock_get.side_effect = requests.exceptions.Timeout
with self.assertRaises(requests.exceptions.Timeout):
get_gold_price("http://fake-api.com")
3 quy tắc “xương máu” khi sử dụng Mock
Sau nhiều năm làm việc với hệ thống lớn, tôi rút ra 3 lưu ý quan trọng để tránh việc Mock làm hỏng logic test:
- Mock đúng vị trí: Hãy patch ở nơi đối tượng được sử dụng, không phải nơi nó được định nghĩa. Nếu file
app.pyimportrequests, bạn cần patch'app.requests.get'. - Đừng lạm dụng: Nếu bạn phải mock quá 5 đối tượng để test một hàm, đó là tín hiệu cho thấy code đang bị phụ thuộc quá chặt chẽ (high coupling). Hãy cân nhắc refactor lại logic.
- Ưu tiên MagicMock: Đây là bản nâng cấp của
Mock, hỗ trợ sẵn các phương thức như__iter__hay__len__, giúp giả lập list hoặc object phức tạp mượt mà hơn.
Làm chủ kỹ thuật Mocking không chỉ giúp CI/CD của bạn luôn xanh rực rỡ. Nó còn giúp bạn tự tin viết code ngay cả khi API của đối tác chưa hoàn thiện. Chúc bạn có những phiên làm việc hiệu quả và không còn lo lắng về những lỗi hệ thống bên ngoài!

