Khóa học
Khi dự án Python của bạn trở nên phức tạp và lớn hơn, tầm quan trọng của kiểm thử vững chắc cũng tăng theo. Kiểm thử giúp đảm bảo mã hoạt động như mong đợi và duy trì chất lượng trên toàn bộ ứng dụng.
Khi ứng dụng của bạn tương tác với các loại cơ sở dữ liệu khác nhau hoặc các API bên ngoài như OpenAI GPT hay Anthropic Claude, việc kiểm thử các phụ thuộc này có thể trở nên khó khăn. Đây là lúc mocking phát huy tác dụng.
Mocking cho phép bạn mô phỏng đối tượng, hàm và hành vi, giúp bài kiểm thử cô lập và dễ dự đoán hơn. Nó cũng cho phép bạn tập trung vào hàm cụ thể cần kiểm thử bằng cách tách nó khỏi các phụ thuộc có thể làm tăng độ phức tạp hoặc gây ra hành vi ngoài ý muốn.
Trong bài viết này, bạn sẽ học cách sử dụng pytest-mock, một plugin mạnh mẽ cho pytest, để triển khai mock trong kiểm thử Python một cách hiệu quả. Đến cuối hướng dẫn, bạn sẽ sẵn sàng bổ sung các kỹ thuật mocking vào test case, giúp chúng mạnh mẽ hơn.
pytest-mock là gì?
pytest-mock là một plugin cho khung kiểm thử Python phổ biến pytest cung cấp khả năng truy cập dễ dàng vào các tính năng mocking. Nó xây dựng dựa trên unittest tích hợp sẵn của Python, đơn giản hóa quá trình mocking trong khi kiểm thử.

Logo khung pytest
pytest-mock cải thiện khả năng đọc và giúp việc triển khai mock trong bài kiểm thử trở nên dễ dàng hơn theo cách tiếp cận “thuần pytest”. Dù bạn cần mock các hàm riêng lẻ, phương thức của lớp, hay toàn bộ đối tượng, pytest-mock đều cung cấp sự linh hoạt cần thiết để kiểm thử hiệu quả mà không thêm phức tạp không cần thiết.
Một lý do quan trọng khiến pytest-mock dễ đọc là phong cách kiểm thử mang tính khai báo, cho phép bạn chỉ định trực tiếp các lời gọi và hành vi mong đợi ngay trong bài kiểm thử. Điều này giúp mã dễ viết, dễ đọc và dễ bảo trì hơn, đồng thời vẫn bền vững trước các thay đổi triển khai không làm thay đổi hành vi bên ngoài của mã.
Thiết lập pytest-mock
Tạo môi trường ảo cho dự án là một thực hành tốt để đảm bảo các phụ thuộc được cô lập. Nếu bạn chưa thiết lập môi trường mới, bạn có thể tạo môi trường conda bằng các lệnh sau:
# create a conda environment
conda create --name yourenvname python=3.11
# activate conda environment
conda activate yourenvname
Cài đặt pytest và pytest-mock
Cài đặt pytest và pytest-mock trực tiếp trong môi trường của bạn bằng pip hoặc conda:
pip install pytest
pip install pytest-mock
Xác nhận cài đặt
Sau khi cài đặt pytest và pytest-mock, hãy xác minh việc cài đặt thành công bằng cách chạy lệnh sau:
pytest --version
Những điều cơ bản về Mocking với pytest-mock
Bạn có thể hiểu mocking như tạo ra một phiên bản “giả” của một thành phần mô phỏng hành vi thật.
Nó đặc biệt hữu ích khi bạn cần kiểm thử cách các hàm hoặc lớp tương tác với các thành phần bên ngoài như cơ sở dữ liệu hoặc API.
Giới thiệu về mocking
Một đối tượng mock mô phỏng hành vi của một đối tượng thật. Nó thường được dùng trong unit test để cô lập các thành phần và kiểm thử hành vi của chúng mà không thực thi mã phụ thuộc.
Ví dụ, bạn có thể dùng một đối tượng mock để mô phỏng một lần gọi cơ sở dữ liệu mà không cần thực sự kết nối tới cơ sở dữ liệu.
Mocking đặc biệt hữu ích khi đối tượng thật:
- Khó tạo hoặc cấu hình.
- Tốn thời gian để sử dụng (ví dụ: truy cập cơ sở dữ liệu từ xa).
- Có tác dụng phụ cần tránh trong quá trình kiểm thử (ví dụ: gửi email, phát sinh chi phí).
Sử dụng fixture mocker
pytest-mock cung cấp fixture mocker, giúp tạo và điều khiển các đối tượng mock một cách dễ dàng. Fixture mocker rất mạnh và tích hợp mượt mà vào các bài kiểm thử pytest của bạn.
💡Fixture là gì?Trong Python, fixture là các thành phần có thể tái sử dụng để thiết lập và thu dọn tài nguyên cần cho kiểm thử, như cơ sở dữ liệu hoặc tệp. Chúng đảm bảo tính nhất quán, giảm trùng lặp và đơn giản hóa thiết lập kiểm thử. |
Hãy xem một ví dụ:
def fetch_weather_data(api_client):
response = api_client.get("https://api.weather.com/data")
if response.status_code == 200:
return response.json()
else:
return None
Hàm fetch_weather_data phụ thuộc vào một API thời tiết bên ngoài để lấy dữ liệu, nhưng gọi API trong lúc kiểm thử có thể phát sinh chi phí không cần thiết.
Để tránh điều này, bạn có thể dùng mocking để mô phỏng hành vi của API trong bài kiểm thử, đảm bảo ứng dụng được kiểm thử mà không cần gọi ra bên ngoài. Dưới đây là cách mocking hỗ trợ:
def test_fetch_weather_data(mocker):
# Create a mock API client
mock_api_client = mocker.Mock()
# Mock the response of the API client
mock_response = mocker.Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"temperature": "20C", "condition": "Sunny"}
# Set the mock API client to return the mocked response
mock_api_client.get.return_value = mock_response
# Call the function with the mock API client
result = fetch_weather_data(mock_api_client)
# Assert that the correct data is returned
assert result == {"temperature": "20C", "condition": "Sunny"}
mock_api_client.get.assert_called_once_with("https://api.weather.com/data")
Trong test_fetch_weather_data(), chúng ta dùng fixture mocker để tạo một API client giả mock_api_client và một mock_response. Phản hồi giả được cấu hình để mô phỏng một lần gọi API thật bằng cách trả về mã trạng thái 200 và dữ liệu JSON chứa thông tin thời tiết.
Phương thức get của API client giả được đặt để trả về phản hồi giả này. Khi kiểm thử fetch_weather_data(), hàm sẽ tương tác với API client giả thay vì thực hiện cuộc gọi API thực.
Cách tiếp cận này đảm bảo hàm hoạt động như mong đợi, đồng thời giữ cho bài kiểm thử nhanh, tiết kiệm chi phí và độc lập với hệ thống bên ngoài.
Câu lệnh assert ở cuối xác minh dữ liệu trả về khớp với kết quả mong đợi.
Dòng cuối cùng, mock_api_client.get.assert_called_once_with("https://api.weather.com/data"), kiểm tra cụ thể rằng phương thức get của API client giả được gọi đúng một lần và với đối số chính xác. Điều này giúp xác nhận hàm tương tác với API đúng như mong đợi, bổ sung một lớp xác thực nữa cho bài kiểm thử.
Các trường hợp sử dụng phổ biến cho pytest-mock
Mocking hữu ích trong nhiều kịch bản, bao gồm unit test nơi bạn cần cô lập các hàm hoặc thành phần cụ thể. Hãy xem một vài trường hợp sử dụng phổ biến:
1. Mocking hàm hoặc phương thức của lớp
Bạn có thể cần kiểm thử một hàm phụ thuộc vào các hàm hoặc phương thức khác, nhưng bạn muốn kiểm soát hành vi của chúng trong quá trình kiểm thử. Mocking cho phép bạn thay thế các phụ thuộc đó bằng hành vi được định nghĩa trước:
# production code
def calculate_discount(price, discount_provider):
discount = discount_provider.get_discount()
return price - (price * discount / 100)
# test code
def test_calculate_discount(mocker):
# Mock the get_discount method
mock_discount_provider = mocker.Mock()
mock_discount_provider.get_discount.return_value = 10 # Mocked discount value
# Call the function with the mocked dependency
result = calculate_discount(100, mock_discount_provider)
# Assert the calculated discount is correct
assert result == 90
mock_discount_provider.get_discount.assert_called_once()
Trong bài kiểm thử này, phương thức get_discount() của discount_provider được mock để trả về một giá trị định trước (10%). Điều này tách biệt hàm calculate_discount() khỏi triển khai thực tế của discount_provider.
2. Mocking mã phụ thuộc thời gian
Khi kiểm thử các hàm có liên quan đến thời gian hoặc độ trễ, mock các phương thức liên quan đến thời gian giúp tránh phải chờ đợi hoặc thao tác với thời gian thực:
# production code
import time
def long_running_task():
time.sleep(5) # Simulate a long delay
return "Task Complete"
# test code
def test_long_running_task(mocker):
# Mock the sleep function in the time module
mocker.patch("time.sleep", return_value=None)
# Call the function
result = long_running_task()
# Assert the result is correct
assert result == "Task Complete"
Ở đây, time.sleep được “patch” để bỏ qua độ trễ thực trong lúc kiểm thử. Điều này giúp bài kiểm thử chạy nhanh mà không cần chờ 5 giây.
3. Mocking thuộc tính đối tượng
Đôi khi, bạn có thể muốn mock hành vi của thuộc tính hoặc property của đối tượng để mô phỏng các trạng thái khác nhau trong quá trình kiểm thử:
# production code
class User:
def __init__(self, name, age):
self.name = name
self.age = age
@property
def is_adult(self):
return self.age >= 18
# test code
def test_user_is_adult(mocker):
# Create a User object
user = User(name="Alice", age=17)
# Mock the is_adult property
mocker.patch.object(User, "is_adult", new_callable=mocker.PropertyMock, return_value=True)
# Assert the mocked property value
assert user.is_adult is True
Trong ví dụ này, property is_adult() của lớp User được mock để trả về True, bất kể độ tuổi thực tế. Điều này hữu ích khi kiểm thử các kịch bản phụ thuộc vào trạng thái của đối tượng.
Kỹ thuật Mocking nâng cao với pytest-mock
Khi bạn đã quen với mocking cơ bản, pytest-mock cung cấp các khả năng nâng cao để xử lý các kịch bản phức tạp hơn.
1. Mock side effect
Mock không chỉ mô phỏng giá trị trả về — chúng còn có thể tái hiện hành vi như phát sinh ngoại lệ hoặc thay đổi giá trị trả về một cách động. Điều này đạt được bằng cách thêm “side effect” vào mock:
import pytest
# production code
def process_payment(payment_gateway, amount):
response = payment_gateway.charge(amount)
if response == "Success":
return "Payment processed successfully"
else:
raise ValueError("Payment failed")
# test code
def test_process_payment_with_side_effects(mocker):
# Mock the charge method of the payment gateway
mock_payment_gateway = mocker.Mock()
# Add side effects: Success on first call, raise exception on second call
mock_payment_gateway.charge.side_effect = ["Success", ValueError("Insufficient funds")]
# Test successful payment
assert process_payment(mock_payment_gateway, 100) == "Payment processed successfully"
# Test payment failure
with pytest.raises(ValueError, match="Insufficient funds"):
process_payment(mock_payment_gateway, 200)
# Verify the mock's behavior
assert mock_payment_gateway.charge.call_count == 2
Thuộc tính side_effect cho phép mock trả về các giá trị khác nhau hoặc phát sinh ngoại lệ ở những lần gọi tiếp theo. Trong ví dụ này, lần gọi đầu tiên tới charge trả về "Success", trong khi lần thứ hai phát sinh ValueError. Điều này hữu ích để kiểm thử nhiều kịch bản trong một bài kiểm thử.
2. Theo dõi (spying) hàm
Spying giúp bạn theo dõi cách một hàm thật được gọi, bao gồm số lần gọi và các tham số truyền vào. Nó đặc biệt hữu ích khi bạn muốn đảm bảo một hàm được gọi như mong đợi trong khi vẫn thực thi logic gốc của nó:
# production code
def log_message(logger, message):
logger.info(message)
return f"Logged: {message}"
# test code
def test_log_message_with_spy(mocker):
# Spy on the info method of the logger
mock_logger = mocker.Mock()
spy_info = mocker.spy(mock_logger, "info")
# Call the function
result = log_message(mock_logger, "Test message")
# Assert the returned value
assert result == "Logged: Test message"
# Verify the spy behavior
spy_info.assert_called_once_with("Test message")
assert spy_info.call_count == 1
Phương thức spy() ghi nhận cách một phương thức thật được gọi mà không thay đổi hành vi của nó. Ở đây, chúng ta theo dõi phương thức info của logger để đảm bảo nó được gọi với thông điệp chính xác, đồng thời vẫn cho phép phương thức thực thi bình thường.
Thực hành tốt nhất khi dùng pytest-mock
Mocking rất mạnh, nhưng nếu dùng thiếu thận trọng có thể gây ra vấn đề trong bài kiểm thử. Dưới đây là một vài thực hành tốt tôi khuyến nghị:
1. Tránh lạm dụng mocking
Dùng quá nhiều mock có thể dẫn tới các bài kiểm thử mong manh, dễ hỏng khi mã bên trong thay đổi, dù hành vi bên ngoài không đổi. Hãy chỉ mock những gì cần thiết và dựa vào triển khai thật khi có thể.
Lạm dụng mocking có thể khiến bài kiểm thử của bạn gắn chặt với chi tiết triển khai, nghĩa là chỉ cần chỉnh sửa nhỏ cũng có thể làm kiểm thử thất bại không cần thiết. Thay vào đó, hãy tập trung mock các phần của hệ thống là bên ngoài hoặc có tác dụng phụ đáng kể.
2. Dùng quy ước đặt tên rõ ràng
Khi mock đối tượng, hãy dùng tên mô tả cho các mock của bạn. Điều này giúp bài kiểm thử dễ đọc và dễ bảo trì hơn. Ví dụ, thay vì dùng các tên chung chung như mock_function(), hãy dùng tên mang tính mô tả hơn, như mock_api_response().
Quy ước đặt tên rõ ràng giúp người khác hiểu mục đích của từng mock, giảm nhầm lẫn và giúp bảo trì bộ kiểm thử theo thời gian dễ dàng hơn.
3. Giữ bài kiểm thử đơn giản và tập trung
Mỗi bài kiểm thử nên tập trung vào một hành vi hoặc kịch bản duy nhất. Điều này đơn giản hóa việc gỡ lỗi và giữ cho bài kiểm thử dễ hiểu. Một bài kiểm thử tập trung tốt sẽ cung cấp phản hồi rõ ràng hơn khi có vấn đề, giúp việc xác định và khắc phục trở nên dễ dàng.
Kết luận
Trong bài viết này, chúng ta đã khám phá những điều cơ bản về mocking với pytest-mock và học cách sử dụng nó để cải thiện kiểm thử Python. Chúng ta đã đề cập từ mocking cơ bản các hàm và phương thức đến các kỹ thuật nâng cao như thêm side effect và theo dõi hàm.
Mocking là công cụ thiết yếu để tạo ra các bài kiểm thử tin cậy và dễ bảo trì, đặc biệt khi làm việc với hệ thống phức tạp hoặc phụ thuộc bên ngoài. Bằng cách tích hợp pytest-mock vào dự án, bạn có thể viết các bài kiểm thử cô lập hơn, dễ dự đoán hơn và dễ bảo trì hơn.
Để tiếp tục nâng cao kỹ năng kiểm thử, tôi khuyên bạn nên tìm hiểu khóa học miễn phí Introduction to Testing in Python trên Datacamp.
Câu hỏi thường gặp
Mocking khác gì so với kiểm thử các thành phần thật?
Mocking thay thế các thành phần thật bằng các thành phần mô phỏng, cho phép kiểm thử cô lập và tiết kiệm chi phí mà không phụ thuộc vào hệ thống bên ngoài hay dữ liệu thật.
Tôi có thể dùng pytest-mock với các khung kiểm thử khác không?
pytest-mock được thiết kế riêng cho pytest và tích hợp liền mạch với nó, nhưng nó xây dựng dựa trên unittest.mock của Python, vốn có thể được dùng độc lập.
Fixture có bắt buộc khi mocking trong pytest-mock không?
Dù các fixture như mocker giúp tăng hiệu quả mocking, bạn vẫn có thể dùng trực tiếp unittest.mock trong pytest mà không cần fixture, dù mức độ tích hợp sẽ kém hơn.
