Giao diện
Thực hành: Testing với Pytest
🎯 Mục tiêu
🎯 Sau bài thực hành này, bạn sẽ:
- Viết test cases có cấu trúc với fixtures và parametrize
- Mock external API calls để test không phụ thuộc network
- Áp dụng Arrange-Act-Assert pattern trong test
Yêu cầu
Bài 1: Viết Test Cases Cơ Bản
Cho module calculator.py, hãy viết test cases đầy đủ.
python
# calculator.py
class Calculator:
def add(self, a: float, b: float) -> float:
return a + b
def divide(self, a: float, b: float) -> float:
if b == 0:
raise ZeroDivisionError("Không thể chia cho 0")
return a / b
def power(self, base: float, exp: int) -> float:
return base ** exp
# test_calculator.py — TODO: Hoàn thành các test
import pytest
class TestCalculator:
# TODO: Tạo fixture cho Calculator instance
def test_add_positive_numbers(self):
pass # Test cộng 2 số dương
def test_divide_by_zero_raises(self):
pass # Test chia cho 0 phải raise ZeroDivisionError
def test_power_with_zero_exponent(self):
pass # Test lũy thừa 0Bài 2: Fixtures và Parametrize
Sử dụng fixtures để chia sẻ setup và parametrize để test nhiều trường hợp.
python
import pytest
@pytest.fixture
def sample_users():
"""Fixture cung cấp dữ liệu test."""
pass # TODO: Trả về list users mẫu
@pytest.mark.parametrize("input_val, expected", [
("hello@example.com", True), ("invalid-email", False),
("user@.com", False), ("a@b.co", True),
])
def test_validate_email(input_val, expected):
pass # TODO: Test hàm validate_email với nhiều inputBài 3: Mock External API
Mock API calls để test không cần kết nối internet.
python
from unittest.mock import patch, MagicMock
class UserService:
def __init__(self, api_url: str):
self.api_url = api_url
def get_user(self, user_id: int) -> dict:
import requests
response = requests.get(f"{self.api_url}/users/{user_id}")
response.raise_for_status()
return response.json()
# TODO: Viết test mock requests.get
# - Test trường hợp thành công
# - Test trường hợp API trả về 404
# - Test trường hợp network errorGợi ý
Gợi ý Bài 1
- Dùng
@pytest.fixtuređể tạo Calculator instance dùng chung pytest.raises(ZeroDivisionError)để test exception- Test edge cases: số âm, số 0, số rất lớn
Gợi ý Bài 2
@pytest.fixturecó thể có scope:function,class,module,session@pytest.mark.parametrizenhận list tuple(input, expected)- Regex cho email:
r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$'
Gợi ý Bài 3
@patch('module.requests.get')thay thếrequests.getbằng mockmock_response.json.return_value = {"id": 1, "name": "Test"}mock_response.raise_for_status.side_effect = HTTPError()cho test lỗi
Lời giải tham khảo
Xem lời giải
python
import pytest
from unittest.mock import patch, MagicMock
# Bài 1 — Fixture + basic tests
class TestCalculator:
@pytest.fixture
def calc(self):
return Calculator()
def test_add_positive(self, calc):
assert calc.add(2, 3) == 5
def test_divide_by_zero(self, calc):
with pytest.raises(ZeroDivisionError, match="Không thể chia cho 0"):
calc.divide(10, 0)
# Mở rộng: test_add_negative, test_power_zero_exp, ...
# Bài 2 — Parametrize
@pytest.mark.parametrize("email, expected", [
("hello@example.com", True), ("invalid-email", False), ("a@b.co", True),
])
def test_validate_email(email, expected):
import re
assert bool(re.match(r'^[\w.+-]+@[\w-]+\.[\w.-]+$', email)) == expected
# Bài 3 — Mock API
class TestUserService:
@patch("requests.get")
def test_get_user_success(self, mock_get):
mock_get.return_value.json.return_value = {"id": 1, "name": "Alice"}
mock_get.return_value.raise_for_status.return_value = None
service = UserService("https://api.example.com")
assert service.get_user(1)["name"] == "Alice"
# Mở rộng: test_not_found (HTTPError), test_network_error