Skip to content

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 0

Bà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 input

Bà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 error

Gợ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.fixture có thể có scope: function, class, module, session
  • @pytest.mark.parametrize nhậ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.get bằng mock
  • mock_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