Chế nhạo bằng Python: unittest.mock, patch và những sai lầm khiến thử nghiệm nói dối
Mô phỏng cho phép bạn kiểm tra mã mà không cần mạng, cơ sở dữ liệu hoặc đồng hồ thực. Sử dụng bất cẩn, họ cũng để mã bị hỏng vượt qua. Đây là cách sử dụng unittest.mock tốt nhất với mỗi lần chạy ví dụ.
- mock đại diện cho một đối tượng thực, chẳng hạn như ứng dụng khách API, vì vậy các thử nghiệm chạy nhanh và ngoại tuyến.
- Sử dụng
return_valuecho câu trả lời cố định vàside_effectcho lỗi hoặc chuỗi. - Vá tên ở nơi nó được sử dụng, chứ không phải ở nơi nó được xác định. Đây là lỗi phổ biến nhất.
- Sử dụng
spechoặcautospecđể tránh lỗi chính tả và đối số sai thay vì chuyển âm thầm.
Hàm của bạn gọi API thời tiết. Bài kiểm tra của bạn không nên phụ thuộc vào Internet, khóa API hoặc thời tiết ngày mai. Tính năng tích hợp sẵn của Python unittest.mock cho phép bạn thay thế lệnh gọi đó bằng một lệnh thay thế trả về chính xác những gì bạn chọn.
Chúng tôi đã viết một mô-đun weather nhỏ và chín bài kiểm tra, tất cả đều chạy bằng Python 3.13 nhỏ nhất. Tất cả chín đều vượt qua và kết quả đầu ra được in bên dưới là từ lần chạy đó.
Tại sao lại chế nhạo?
- Tốc độ: không cần gọi mạng hoặc thiết lập cơ sở dữ liệu.
- Độ tin cậy: các thử nghiệm không thất bại do dịch vụ của bên thứ ba không hoạt động.
- Kiểm soát: bạn có thể mô phỏng thời gian chờ, lỗi và dữ liệu kỳ lạ theo yêu cầu.
- Cách ly: bạn kiểm tra logic của mình chứ không phải máy chủ của người khác.
Mô phỏng chủ yếu thuộc về kiểm thử đơn vị. Kiểm tra tích hợp vẫn phải nói chuyện với các phần phụ thuộc thực tế hoặc thực tế.
Mô phỏng cơ bản
from unittest.mock import Mock
api = Mock()
api.get_price.return_value = 499
print(api.get_price("pen")) # 499
api.get_price.assert_called_once_with("pen")
print(api.get_price.call_args_list) # [call('pen')]
print(api.anything_at_all()) # <Mock name='mock.anything_at_all()' ...>
Mock ghi lại mọi cuộc gọi để bạn có thể kiểm tra xem nó được sử dụng như thế nào. Tuy nhiên, hãy chú ý đến dòng cuối cùng. Một Mock đơn giản sẽ phát minh ra bất kỳ thuộc tính nào bạn yêu cầu. Sự linh hoạt đó rất thuận tiện nhưng cũng nguy hiểm, như chúng ta sẽ thấy.
MagicMock là ý tưởng tương tự nhưng cũng hỗ trợ các phương thức “ma thuật” như __len__ và __iter__. patch() sử dụng MagicMock theo mặc định.
side_effect: lỗi và trình tự
side_effect kiểm soát những gì xảy ra trên mỗi cuộc gọi. Đặt nó thành một ngoại lệ để mô phỏng lỗi:
api.fetch.side_effect = ConnectionError("down")
api.fetch() # raises ConnectionError
Đặt nó vào danh sách để trả về các kết quả khác nhau trong các cuộc gọi liên tiếp. Điều này là hoàn hảo để thử nghiệm logic thử lại:
def fetch_with_retry(api, attempts=3):
for _ in range(attempts):
try:
return api.fetch()
except ConnectionError:
continue
raise ConnectionError("all attempts failed")
api = Mock()
api.fetch.side_effect = [ConnectionError("busy"), {"ok": True}]
print(fetch_with_retry(api)) # {'ok': True}
print(api.fetch.call_count) # 2
Cuộc gọi đầu tiên không thành công, cuộc gọi thứ hai thành công và quá trình kiểm tra đã chứng minh chức năng được thử lại chính xác một lần. Việc mô phỏng điều này với một dịch vụ thực tế là gần như không thể.
patch(): thay thế tạm thời một cái gì đó
Thường thì phần phụ thuộc không được truyền vào. Nó được nhập vào bên trong mô-đun. patch() hoán đổi nó trong suốt thời gian thử nghiệm và khôi phục nó sau đó:
# weather.py
from http_client import get as requests_get
def report(city):
data = requests_get(f"https://api.example.com/weather?city={city}")
return f"{city}: {data['temp_c']}°C"
from unittest.mock import patch
with patch("weather.requests_get") as fake_get:
fake_get.return_value = {"temp_c": 31}
print(weather.report("Pune")) # Pune: 31°C
fake_get.assert_called_once_with(
"https://api.example.com/weather?city=Pune")
Bạn có thể sử dụng patch làm trình quản lý bối cảnh như thế này hoặc làm công cụ trang trí, chuyển mô hình này làm đối số.
Bản vá nơi nó được sử dụng, không phải nơi nó được xác định
Đây là sai lầm hầu như ai cũng mắc phải một lần. Hàm get thực sự tồn tại trong http_client, vì vậy việc vá nó ở đó là điều tự nhiên:
with patch("http_client.get") as fake_get:
fake_get.return_value = {"temp_c": 99}
weather.report("Pune")
# ConnectionError: no network in tests: https://api.example.com/weather?city=Pune
Chức năng thực sự đã chạy. Khi weather.py thực hiện from http_client import get as requests_get, nó tạo ra tên riêng trỏ vào hàm ban đầu. Bản vá http_client.get đã đổi tên khác nên weather chưa bao giờ thấy bản mô phỏng. http_client.get của chúng tôi cố tình tăng cược nên sai sót rất lớn. Trong một dự án thực, nó có thể lặng lẽ gọi một API trực tiếp.
Quy tắc: vá tên trong mô-đun đang thử nghiệm, viết là "module_under_test.name". Nếu mô-đun đã thực hiện import http_client và gọi http_client.get() thì việc vá lỗi "http_client.get" là chính xác vì việc tra cứu diễn ra tại thời điểm gọi.
spec và autospec: làm cho các bản mô phỏng trở nên nghiêm ngặt
Một Mock đơn giản chấp nhận mọi thứ, kể cả lỗi chính tả. Với một thông số kỹ thuật, nó chỉ cho phép các thuộc tính mà lớp thực có:
api = Mock(spec=weather.WeatherClient)
api.get_temprature("Pune")
# AttributeError: Mock object has no attribute 'get_temprature'
loose = Mock()
loose.get_temprature("Pune") # returns a Mock, no error at all
Bản mô phỏng lỏng lẻo vui vẻ chấp nhận phương pháp sai chính tả, vì vậy một bài kiểm tra sử dụng nó có thể vượt qua mặc dù mã thực sẽ bị lỗi. autospec đi xa hơn và kiểm tra chữ ký phương thức:
with patch("weather.WeatherClient", autospec=True) as Fake:
client = Fake.return_value
client.get_temperature("Pune", "extra", "args")
# TypeError: too many positional arguments
Thích autospec=True hoặc create_autospec() khi chế nhạo lớp học của riêng bạn. Nó giữ cho các mô tả trung thực khi mã thực thay đổi.
Ngày và giờ chế nhạo
Mã phụ thuộc vào ngày hôm nay rất khó kiểm tra. Bọc đồng hồ trong một chức năng nhỏ và vá:
def today():
return datetime.date.today()
def is_weekend():
return today().weekday() >= 5
@patch("weather.today")
def test_date(self, fake_today):
fake_today.return_value = datetime.date(2026, 11, 7)
print(weather.is_weekend()) # True (a Saturday)
Bạn không thể dễ dàng vá trực tiếp datetime.date.today vì các loại tích hợp không cho phép đặt thuộc tính của chúng. Các thư viện như Freezegun và Time-machine xử lý việc này một cách đầy đủ hơn.
Khẳng định trên mô hình
| phương pháp | Séc |
|---|---|
assert_called() |
Đã gọi ít nhất một lần |
assert_called_once() |
Được gọi đúng một lần |
assert_called_with(...) |
Cuộc gọi cuối cùng đã sử dụng các đối số này |
assert_called_once_with(...) |
Chính xác một cuộc gọi, với những đối số này |
assert_any_call(...) |
Bất kỳ cuộc gọi nào đều sử dụng các đối số này |
assert_not_called() |
Chưa bao giờ gọi |
Khi một xác nhận không thành công, thông báo sẽ rõ ràng. Chúng tôi đã gọi assert_called_once() trên một phương thức chưa bao giờ chạy:
AssertionError: Expected 'save' to have been called once. Called 0 times.
Các phiên bản Python cũ hơn cho phép các phương thức xác nhận sai chính tả, chẳng hạn như assret_called_once, được chuyển âm thầm. Các phiên bản hiện đại có lỗi đối với các tên bắt đầu bằng assert hoặc các lỗi chính tả phổ biến của nó, nhưng thông số kỹ thuật vẫn là biện pháp bảo vệ mạnh mẽ hơn.
pytest và mocks
Mọi thứ ở trên cũng hoạt động trong pytest. Nhiều người dùng pytest cũng sử dụng công cụ cố định monkeypatch tích hợp để thay thế đơn giản hoặc plugin pytest-mock, cung cấp công cụ cố định mocker tự động hoàn tác các bản vá. Quy tắc “bản vá được sử dụng” áp dụng cho tất cả chúng. Xem hướng dẫn pytest của chúng tôi để biết cách thiết lập và cố định.
Khi nào không nên chế giễu
- Đừng chế nhạo những gì bạn đang thử nghiệm. Nếu mô hình làm được tất cả công việc thì bài kiểm tra không chứng minh được điều gì.
- Tránh chế giễu các đối tượng có giá trị đơn giản. Hãy sử dụng đồ thật.
- Đừng mô phỏng mọi thứ. Các thử nghiệm chứa đầy mô phỏng thường bị hỏng bất cứ khi nào mã được tái cấu trúc, ngay cả khi hành vi đúng.
- Giữ một số thử nghiệm tích hợp thực tế. Bản mô phỏng mã hóa các giả định của bạn về một dịch vụ và những giả định đó có thể sai.
Mô phỏng cũng dễ dàng tăng cường phạm vi bảo hiểm của mã vì các đường dẫn mô phỏng chạy mà không có bất kỳ hành vi thực sự nào đằng sau chúng.
Những lỗi chế giễu thường gặp
- Đổ sai tên. Đã đề cập ở trên và vẫn là nguyên nhân số một khiến “bản mô phỏng của tôi bị bỏ qua”.
- Quên đặt return_value. Mã được kiểm tra sau đó nhận được một đối tượng Mock thay vì dữ liệu. Nó có thể đi thẳng qua hàm của bạn và vô tình vượt qua bài kiểm tra, chẳng hạn như khi bạn chỉ kiểm tra xem kết quả có đúng hay không.
- Xác nhận trên bản mô phỏng thay vì kết quả. Kiểm tra xem
save()đã được gọi yếu hơn so với kiểm tra những gì đã được lưu. Sử dụngassert_called_once_with(...)với các đối số thực hoặc tốt hơn là xác nhận kết quả của hàm. - Các bản vá bị rò rỉ. Gọi
patcher.start()mà không cóstop()sẽ để lại bản giả tại chỗ cho các lần kiểm tra sau. Sử dụng trình quản lý bối cảnh, trình trang trí hoặcaddCleanup(patcher.stop). - Các trình trang trí được xếp chồng lên nhau không đúng thứ tự. Với một số trình trang trí
@patch, trình trang trí ở dưới cùng được chuyển làm đối số đầu tiên. Việc trộn chúng một cách âm thầm sẽ cấu hình sai mô hình. - Chế nhạo thư viện mà bạn không sở hữu qua nhiều bài kiểm tra. Thay vào đó, hãy gói nó trong một lớp học nhỏ của riêng bạn và mô phỏng nó. Sau đó, các bài kiểm tra của bạn sẽ mô tả nhu cầu của bạn chứ không phải API của thư viện.
Giả, sơ khai và giả mạo
Những từ này thường được sử dụng một cách lỏng lẻo, nhưng sự khác biệt sẽ giúp ích khi thảo luận về các bài kiểm tra. sơ khai chỉ trả về các câu trả lời soạn sẵn. mock cũng ghi lại các cuộc gọi để bạn có thể xác minh cách nó được sử dụng. fake là cách triển khai đơn giản, đang hoạt động, chẳng hạn như kho lưu trữ trong bộ nhớ thay vì cơ sở dữ liệu. Mock của Python có thể hoạt động như một bản sơ khai hoặc một bản mô phỏng. Hàng giả là các lớp thông thường mà bạn tự viết và chúng thường dẫn đến các thử nghiệm tồn tại trong quá trình tái cấu trúc tốt hơn, vì chúng kiểm tra hành vi thay vì các lệnh gọi chính xác.
Một nguyên tắc nhỏ là thử nghiệm các khía cạnh của hệ thống của bạn: mạng, đồng hồ, số ngẫu nhiên, hệ thống tệp và các dịch vụ trả phí của bên thứ ba. Bên trong những cạnh đó, thích đồ vật thật hơn. Các thử nghiệm được xây dựng theo cách này sẽ hoạt động nhanh và vẫn thực thi mã bạn thực sự đã viết.
Lời kết
Mocking là một công cụ chính xác: thay thế các phần phụ thuộc chậm hoặc không thể đoán trước, kiểm soát những gì chúng trả về và kiểm tra cách mã của bạn sử dụng chúng. Hai thói quen ngăn ngừa hầu hết các vấn đề. Vá tên trong mô-đun đang được kiểm tra và sử dụng autospec để các cuộc gọi mô phỏng của bạn từ chối đối tượng thực sẽ từ chối.
Câu hỏi thường gặp
Chế giễu trong Python là gì?
Chế độ mô phỏng thay thế một đối tượng thực, chẳng hạn như ứng dụng khách API hoặc cơ sở dữ liệu, bằng một đối tượng giả mà bạn kiểm soát trong quá trình thử nghiệm. Python bao gồm điều này trong mô-đun unittest.mock.
Sự khác biệt giữa Mock và MagicMock là gì?
MagicMock là một Mô phỏng cũng hỗ trợ các phương pháp ma thuật như __len__, __iter__ và __enter__. patch() tạo MagicMock theo mặc định.
Tại sao bản vá của tôi không hoạt động?
Thông thường là do bạn đã vá nơi đối tượng được xác định thay vì nơi nó được sử dụng. Vá tên bên trong mô-đun đang được thử nghiệm.
Sự khác biệt giữa return_value và side_effect là gì?
return_value đặt kết quả cố định. side_effect có thể đưa ra một ngoại lệ, trả về các giá trị từ danh sách theo thứ tự hoặc gọi một hàm.
Autospec làm gì?
Nó xây dựng một bản mô phỏng từ đối tượng thực, do đó, việc thiếu các thuộc tính và đối số sai sẽ gây ra lỗi, giống như với mã thực.
Tiếp tục học hỏi
Cần trợ giúp về kiểm tra, WordPress hoặc SEO?
Chúng tôi giúp các nhóm phát triển phần mềm tốt hơn và phát triển trang web của họ. Hãy cho chúng tôi biết bạn đang làm gì và chúng tôi sẽ đề xuất bước tiếp theo rõ ràng.
Thêm hướng dẫn về thử nghiệm, AI và phát triển hiện đại
Bạn thích bài viết này? Chọn một chủ đề và chuyển ngay sang phần hướng dẫn mới nhất của chúng tôi.
Giải thích về ảo giác AI: Tại sao các mô hình ngôn ngữ lại tạo nên mọi thứ và cách giảm thiểu nó
Ảo giác AI đã giải thích: chúng là gì, tại sao mô hình ngôn ngữ tạo ra chúng, các loại phổ biến, cách giảm thiểu chúng bằng cách căn cứ và nhắc nhở …
Đọc bài viết →DevOpsGiải thích về các biến môi trường: Tệp .env, bí mật và cấu hình được thực hiện đúng
Các biến môi trường được giải thích bằng các ví dụ Python mà chúng tôi đã chạy: đọc và thiết lập chúng, tại sao mọi giá trị đều là một chuỗi, tệp .e…
Đọc bài viết →Phát triển phần mềmGiải thích về giới hạn tỷ lệ API: Thuật toán, tiêu đề và xử lý lỗi 429
Giới hạn tốc độ API được giải thích bằng mô phỏng mà chúng tôi đã chạy: nhóm mã thông báo, cửa sổ cố định, cửa sổ trượt, 429 phản hồi, tiêu đề giới …
Đọc bài viết →Tự động hóa kiểm thửChế giễu bằng Python: unittest.mock, patch và những sai lầm khiến bài kiểm tra nói dối
Mô phỏng bằng Python với unittest.mock, đã được thử nghiệm: Mock và return_value, side_effect, vá lỗi nơi sử dụng tên, thông số kỹ thuật và thông số…
Đọc bài viết →Kiểm thử phần mềmGiải thích về phạm vi bảo hiểm của mã: Phạm vi tuyến so với chi nhánh và ý nghĩa thực sự 100%
Phạm vi bảo hiểm mã được giải thích bằng cách chạy phạm vi bảo hiểm thực tế: phạm vi bảo hiểm dòng, chi nhánh và điều kiện, đọc báo cáo, lý do tại s…
Đọc bài viết →Cơ sở dữ liệu & SQLDELETE vs TRUNCATE vs DROP trong SQL: Sự khác biệt, khôi phục và thời điểm sử dụng mỗi loại
DELETE vs TRUNCATE vs DROP được giải thích bằng SQL đã được thử nghiệm: mỗi loại loại bỏ, khôi phục, kích hoạt, đặt lại danh tính, tốc độ và Postgre…
Đọc bài viết →Cơ sở dữ liệu & SQLGiải thích về Chế độ xem SQL: Tạo, sử dụng và tránh những cạm bẫy, kèm theo ví dụ
Chế độ xem SQL được giải thích bằng các truy vấn chúng tôi đã chạy: tạo chế độ xem, kết quả trực tiếp, ẩn cột, hiệu suất, chế độ xem có thể cập nhật…
Đọc bài viết →Phát triển phần mềmGiao diện TypeScript so với loại: Sự khác biệt thực sự, được thử nghiệm bằng trình biên dịch
Giao diện TypeScript so với loại, được kiểm tra bằng trình biên dịch thực: cả hai có thể làm gì, chỉ một người có thể làm gì, hợp nhất khai báo, mở …
Đọc bài viết →Phát triển phần mềmGiải thích về Python *args và **kwargs: Hàm linh hoạt với ví dụ rõ ràng
Python *args và **kwargs được giải thích bằng mã chúng tôi đã chạy: đóng gói và giải nén, thứ tự tham số, đối số chỉ từ khóa và chỉ vị trí, trình tr…
Đọc bài viết →Phát triển phần mềmHàm Lambda của Python: Cú pháp, cách sử dụng thực tế và khi nào không nên sử dụng chúng
Các hàm lambda của Python được giải thích bằng mã chúng tôi đã chạy: cú pháp, sắp xếp bằng khóa, bản đồ và bộ lọc, các phần đóng, bẫy liên kết muộn …
Đọc bài viết →Trí tuệ nhân tạoĐại lý AI là gì? Cách chúng hoạt động, với vòng lặp tác nhân đơn giản, bạn có thể chạy
Tác nhân AI là gì? Hướng dẫn đơn giản về vòng lặp tác nhân, công cụ, bộ nhớ và lan can, với một vòng lặp Python nhỏ mà chúng tôi đã chạy, những rủi …
Đọc bài viết →DevOpsGiám sát và Khả năng quan sát: Sự khác biệt thực sự, với nhật ký, số liệu và dấu vết
Giải thích về giám sát và khả năng quan sát: lỗi đã biết và chưa biết, nhật ký, số liệu và dấu vết, hướng dẫn về sự cố thanh toán, tín hiệu vàng, Op…
Đọc bài viết →DevOpsDocker Volumes vs Bind Mounts: Duy trì dữ liệu vùng chứa đúng cách
Giải thích về khối lượng Docker và liên kết gắn kết: tại sao dữ liệu vùng chứa biến mất, khối lượng được đặt tên, liên kết gắn kết, tmpfs, ví dụ về …
Đọc bài viết →Tự động hóa kiểm thửTDD vs BDD: Key Differences, One Feature Built Both Ways and When to Use Each
TDD và BDD được giải thích bằng một tính năng giảm giá được xây dựng theo cả hai cách: tái cấu trúc màu đỏ-xanh với mã chúng tôi đã chạy, kịch bản C…
Đọc bài viết →Phát triển phần mềmGit merge vs rebase: khác biệt, ví dụ và khi nào dùng cái nào
Git merge vs rebase được giải thích bằng đầu ra log thực tế, bảng so sánh, quy tắc vàng của rebase và quy trình làm việc nhóm an toàn.
Đọc bài viết →Phát triển phần mềmList comprehension trong Python: cú pháp, ví dụ và khi nào nên tránh
List comprehension trong Python: cú pháp, lọc dữ liệu, if-else, vòng lặp lồng nhau, thời gian đo thực tế và khi nào vòng lặp for thông thường tốt hơn.
Đọc bài viết →Cơ sở dữ liệu & SQLGiải thích SQL JOIN kèm ví dụ (Inner, Left, Right, Full)
Hiểu SQL JOIN từng bước với bảng và truy vấn mẫu: INNER, LEFT, RIGHT, FULL, CROSS và SELF, cùng lỗi thường gặp và mẹo hiệu năng.
Đọc bài viết →