Ruff, mypy và pytest: bộ công cụ giữ chất lượng code Python

Code chạy được không có nghĩa là code tốt. Khi dự án lớn dần và có nhiều người cùng làm, những công cụ kiểm tra tự động giúp giữ chất lượng ổn định mà không phải tranh luận trong từng lần review.

Ruff — kiểm tra và định dạng

Ruff viết bằng Rust, thay thế được gần như toàn bộ các công cụ kiểm tra code trước đây, và chạy nhanh hơn nhiều lần.

pip install ruff

ruff check .              # tìm lỗi
ruff check --fix .        # tự sửa những gì sửa được
ruff format .             # định dạng code

Cấu hình đặt trong pyproject.toml:

[tool.ruff]
line-length = 100
target-version = "py312"

[tool.ruff.lint]
select = [
    "E",    # lỗi kiểu pycodestyle
    "F",    # lỗi logic (pyflakes)
    "I",    # sắp xếp import
    "B",    # bug tiềm ẩn (bugbear)
    "UP",   # cú pháp lỗi thời
    "SIM",  # đơn giản hóa code
]
ignore = ["E501"]

Nhóm quy tắc B đáng chú ý — nó bắt được những lỗi thật sự, không chỉ vấn đề hình thức. Ví dụ nó phát hiện ngay lỗi dùng list làm giá trị mặc định cho tham số hàm.

mypy — kiểm tra kiểu tĩnh

Python không kiểm tra kiểu khi chạy, nhưng mypy kiểm tra được trước khi chạy:

pip install mypy
mypy .
def tinh_tong(so: list[int]) -> int:
    return sum(so)

tinh_tong(["a", "b"])     # mypy báo lỗi ngay, không cần chạy

Với dự án đang có sẵn, đừng bật chế độ nghiêm ngặt ngay lập tức — bạn sẽ nhận hàng nghìn cảnh báo và bỏ cuộc. Hãy siết dần:

[tool.mypy]
python_version = "3.12"
warn_return_any = true
warn_unused_configs = true
ignore_missing_imports = true

# Siết chặt từng module một
[[tool.mypy.overrides]]
module = "myapp.core.*"
disallow_untyped_defs = true

pytest — viết kiểm thử

pip install pytest pytest-cov
# test_tinh_toan.py
import pytest
from myapp import tinh_giam_gia

def test_giam_gia_thong_thuong():
    assert tinh_giam_gia(100_000, 10) == 90_000

def test_giam_gia_bang_khong():
    assert tinh_giam_gia(100_000, 0) == 100_000

def test_phan_tram_am_bi_tu_choi():
    with pytest.raises(ValueError, match="không hợp lệ"):
        tinh_giam_gia(100_000, -5)

@pytest.mark.parametrize("gia,phan_tram,mong_doi", [
    (100_000, 10, 90_000),
    (200_000, 50, 100_000),
    (50_000, 100, 0),
])
def test_nhieu_truong_hop(gia, phan_tram, mong_doi):
    assert tinh_giam_gia(gia, phan_tram) == mong_doi

parametrize giúp bạn kiểm thử nhiều trường hợp mà không lặp code. Mỗi bộ tham số được tính là một bài kiểm thử riêng, nên khi có lỗi bạn biết chính xác trường hợp nào hỏng.

pytest -v
pytest --cov=myapp --cov-report=term-missing

Về độ bao phủ: con số cao không đảm bảo chất lượng. Một bài kiểm thử chạy qua code mà không kiểm tra kết quả vẫn làm tăng độ bao phủ. Hãy dùng chỉ số này để tìm phần chưa được kiểm thử, đừng biến nó thành mục tiêu.

pre-commit — chạy tự động trước mỗi commit

Công cụ chỉ hữu ích khi thực sự được chạy. pre-commit đảm bảo điều đó:

pip install pre-commit

Tạo file .pre-commit-config.yaml:

repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.6.9
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format

  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-added-large-files
      - id: detect-private-key
pre-commit install
pre-commit run --all-files

Hook detect-private-key đáng giá riêng nó: nó chặn bạn vô tình commit khóa riêng tư lên kho mã nguồn — một sự cố bảo mật rất khó khắc phục sau khi đã xảy ra.

Chạy trên CI

Kiểm tra ở máy cá nhân có thể bị bỏ qua, CI thì không. Ví dụ với GitHub Actions:

name: Kiểm tra chất lượng

on: [push, pull_request]

jobs:
  kiem-tra:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - run: pip install ruff mypy pytest pytest-cov
      - run: ruff check .
      - run: ruff format --check .
      - run: mypy .
      - run: pytest --cov=myapp

Rà soát bảo mật phụ thuộc

Thư viện bạn dùng cũng có thể chứa lỗ hổng đã được công bố:

pip install pip-audit
pip-audit

Công cụ này đối chiếu danh sách phụ thuộc của bạn với cơ sở dữ liệu lỗ hổng đã biết. Nên chạy định kỳ trên CI, không chỉ một lần lúc bắt đầu dự án.

Bắt đầu từ đâu

Với dự án mới, hãy thiết lập tất cả ngay từ ngày đầu — lúc đó chi phí gần như bằng không.

Với dự án đang chạy, đừng cố sửa mọi thứ cùng lúc. Thêm Ruff trước với một tập quy tắc nhỏ, cho nó chạy tự động, rồi mở rộng dần. Một pull request sửa hai nghìn dòng chỉ để định dạng lại code sẽ khiến việc xem lại lịch sử thay đổi trở nên khó khăn về sau.