FDE PulseViệc làm FDE đang mở 641Mới trong 7 ngày 48Công ty đang tuyển 98Nhận làm từ xa 25%Lương trung vị (Mỹ) $203kTuyển nhiều nhất Databricks 122
EN

Tờ báo của nghề Forward Deployed Engineer

Bách khoa

Thực hành VCR.py và pytest: ghi API của khách một lần, chạy test offline mãi về sau

Sandbox của khách có thể sập, token có thể hết hạn, nhưng bộ test tích hợp của bạn vẫn phải xanh trong CI mà không lộ một byte dữ liệu nào của khách.

Ảnh kỹ sư đang viết code Python trên laptop, màn hình hiện mã nguồn và terminal, bàn làm việc gọn gàng.
Ảnh: Emile Perron / Unsplash
Chia sẻLinkedInFacebookX

Tóm tắt nhanh

  • Ghi request thật vào cassette một lần, sau đó test phát lại response mà không cần mạng hay token.
  • Lọc header, query string và nội dung response trước khi commit, vì cassette là file văn bản ai mở cũng đọc được.
  • Trong CI, để pytest-recording chạy ở chế độ none mặc định và thêm --block-network để mọi request lạ đều báo lỗi.
Bảng năm chế độ ghi của VCR.py: once chỉ ghi khi chưa có cassette, dùng khi ghi lần đầu; new_episodes phát lại và ghi thêm, dùng khi thêm endpoint; none phát lại nhưng không ghi mới, được tô nổi bật, dùng cho CI và hằng ngày; all luôn ghi, không phát lại, hiếm khi dùng; rewrite ghi lại từ đầu, dùng khi khách đổi schema.
Chỉ chế độ none không bao giờ ghi mới. Request nào chưa có trong cassette sẽ làm test đỏ, nên CI không thể âm thầm gọi API thật của khách. Nguồn: tài liệu VCR.py và pytest-recording, theo bài viết.

Thử hình dung: tuần thứ hai ở site khách, sandbox API của họ bắt đầu trả lỗi 503 vào mỗi buổi chiều. Lần nào cũng vậy, CI đỏ, dù code của bạn không đổi một dòng.

Cách xử lý gọn nhất là gọi API thật đúng một lần, ghi lại toàn bộ request và response, rồi để mọi lần chạy sau dùng bản ghi đó. VCR.py làm đúng việc này. Theo tài liệu của thư viện, lần chạy đầu tiên của test sẽ ghi request HTTP vào một file cassette dạng YAML, và từ đó trở đi test đọc lại từ file.

Bài này dựng một ví dụ hoàn chỉnh: client cho API đơn hàng giả định của khách, test pytest ghi một lần, dữ liệu nhạy cảm được che trước khi commit, và CI chạy hoàn toàn offline. Đây là thứ bạn sẽ cần ngay trong tuần đầu của một engagement tích hợp.

Cần gì trước khi bắt đầu?

Bạn cần Python, pytest, thư viện requests và hai gói: vcrpy cùng pytest-recording. Gói thứ hai là plugin pytest chạy trên nền VCR.py, cho phép đánh dấu test bằng marker pytest.mark.vcr thay vì tự bọc từng đoạn code.

pip install pytest requests vcrpy pytest-recording

Endpoint api.example-customer.com dưới đây là tên giả định. Khi làm thật, bạn thay bằng sandbox của khách và một token họ cấp.

Bước 1: viết client mỏng, tách khỏi test

Giữ mọi lời gọi HTTP trong một module riêng. Test sẽ gọi hàm này giống hệt cách code production gọi.

# orders_client.py
import requests

BASE_URL = "https://api.example-customer.com"

def list_orders(token, status="open"):
    resp = requests.get(
        f"{BASE_URL}/v1/orders",
        params={"status": status},
        headers={"Authorization": f"Bearer {token}"},
        timeout=10,
    )
    resp.raise_for_status()
    return resp.json()["orders"]

Cần kiểm tra: chạy thử hàm này một lần từ terminal với token thật để chắc rằng API trả về danh sách đơn hàng. Nếu bước này lỗi thì đừng ghi gì cả, vì cassette sẽ lưu luôn cái lỗi đó.

Bước 2: ghi cassette đầu tiên bằng VCR.py thuần

Trước khi dùng plugin, hãy xem VCR.py hoạt động ra sao ở mức thấp nhất.

# test_raw.py
import os
import vcr
from orders_client import list_orders

def test_list_orders_raw():
    with vcr.use_cassette("fixtures/vcr_cassettes/orders.yaml"):
        orders = list_orders(token=os.environ["CUSTOMER_TOKEN"])
    assert isinstance(orders, list)

Chế độ ghi mặc định của VCR.py là once: nếu file cassette chưa tồn tại thì nó ghi tương tác mới, còn nếu đã có thì chỉ phát lại. Chạy test hai lần, lần thứ hai sẽ nhanh hơn hẳn vì không còn đi qua mạng.

Bây giờ hãy mở orders.yaml. Bạn sẽ thấy header Authorization cùng token nằm nguyên văn trong file, kèm toàn bộ nội dung đơn hàng của khách. Ghi lại vài địa chỉ email thật và tên miền email bạn thấy ở đây, lát nữa sẽ dùng để kiểm tra. Đừng commit file này. Xoá nó đi, rồi chuyển sang bước 3.

Bước 3: chuyển sang pytest-recording và lọc bí mật

Với pytest-recording, cấu hình ghi nằm trong fixture vcr_config. Đây cũng là chỗ khai báo những gì không được phép lọt vào cassette.

# conftest.py
import pytest
from scrub import scrub_response

@pytest.fixture(scope="module")
def vcr_config():
    return {
        "filter_headers": ["authorization"],
        "filter_query_parameters": ["api_key"],
        "before_record_response": scrub_response,
    }

filter_headers nhận một danh sách header cần bỏ khỏi bản ghi, còn filter_query_parameters làm việc tương tự với query string. Tham số thứ hai cần có khi khách truyền khoá qua URL kiểu ?api_key=..., chuyện vẫn gặp ở các hệ thống cũ.

# test_orders.py
import os
import pytest
from orders_client import list_orders

@pytest.mark.vcr
def test_list_open_orders():
    token = os.environ.get("CUSTOMER_TOKEN", "dummy")
    orders = list_orders(token=token)
    assert all(o["status"] == "open" for o in orders)

Lưu ý giá trị "dummy": khi phát lại, test không cần token thật. Trên CI không có secret nào, và đó chính là điều bạn muốn.

Bước 4: che dữ liệu của khách trong response

Lọc header mới xử lý được nửa vấn đề. Phần body response vẫn chứa email, tên, địa chỉ của khách hàng cuối. Hook before_record_response cho phép sửa response trước khi ghi, hoặc trả về None để bỏ hẳn cả cặp request và response đó khỏi cassette.

# scrub.py
import json

def scrub_response(response):
    body = response["body"]["string"]
    try:
        data = json.loads(body)
    except ValueError:
        return response
    for order in data.get("orders", []):
        order["customer_email"] = "[email protected]"
    response["body"]["string"] = json.dumps(data).encode()
    return response

Đoạn này đã được đơn giản hoá. Nó giả định body là JSON chưa nén và trường cần che tên là customer_email. Với API thật, hãy hỏi khách trường nào là dữ liệu cá nhân thay vì tự đoán.

Giờ ghi lại. pytest-recording mặc định chạy ở chế độ none để không gọi mạng ngoài ý muốn, nên bạn phải chủ động bật chế độ ghi. Vì cấu hình ghi của plugin nằm trong vcr_config, cách dễ theo dõi nhất là tạm đặt chế độ once ngay trong fixture đó trên máy bạn, chạy test một lần với token thật, rồi bỏ dòng ấy đi trước khi commit.

CUSTOMER_TOKEN=xxx pytest test_orders.py

Cần kiểm tra: mở file cassette vừa sinh ra, tìm chữ Bearer, tìm token, rồi tìm tên miền email thật của khách và những địa chỉ email bạn đã ghi lại ở bước 2. Đừng tìm ký tự @, vì chính giá trị thay thế [email protected] cũng chứa nó. Không còn kết quả nào thì mới được commit.

Bước 5: khoá CI vào chế độ offline

Trong CI, bạn không cần cấu hình gì thêm cho chế độ ghi, vì none đã là mặc định. Chế độ này báo lỗi với mọi request mới, nên nếu ai đó thêm một lời gọi API mà quên ghi cassette, test sẽ đỏ ngay chứ không lặng lẽ gọi ra ngoài.

pytest --block-network

Cờ --block-network chặn toàn bộ truy cập mạng trong lúc chạy test, kể cả những test không gắn marker vcr. Nó là lớp bảo vệ thứ hai, bắt được các lời gọi mạng nằm ngoài đường đi của VCR.

Khi API của khách đổi, ghi lại thế nào?

Đây là lúc nhiều người làm hỏng cassette. Mỗi chế độ ghi phục vụ một tình huống khác nhau, và chọn sai có thể khiến CI vô tình gọi mạng thật.

Chế độ Hành vi Dùng khi
once Chỉ ghi khi chưa có cassette Ghi lần đầu trên máy bạn
new_episodes Phát lại cái cũ, ghi thêm tương tác mới Thêm một endpoint vào test có sẵn
none Báo lỗi với mọi request mới CI và mọi lần chạy thường ngày
all Không bao giờ phát lại, luôn ghi Hiếm khi; gần như không bao giờ trong CI
rewrite Ghi lại cassette từ đầu (pytest-recording) Khách đổi schema, cần bản ghi sạch

Khi khách báo đã đổi schema, hãy chạy rewrite trên máy cá nhân, mở file kiểm tra bí mật như ở bước 4, rồi commit trong một pull request riêng. Diff của cassette lúc đó cho bạn thấy chính xác API đã thay đổi những gì.

Những lỗi hay gặp

Lỗi đầu tiên liên quan đến cách khớp request. Mặc định VCR.py khớp theo method, scheme, host, port, path và query, tức là không xét body. Hai lời gọi POST /v1/orders với body khác nhau có thể bị coi là một, và test của bạn nhận nhầm response.

Khi cần test nhiều POST trên cùng URL, cách an toàn là cho mỗi lời gọi một file cassette riêng bằng vcr.use_cassette như ở bước 2.

Lỗi thứ hai là thêm filter sau khi đã commit cassette. Filter chỉ có tác dụng với lần ghi tiếp theo, không xoá được token đã nằm trong lịch sử git. Nếu chuyện đó xảy ra, hãy báo khách để họ thu hồi token, đừng chỉ sửa file.

Lỗi thứ ba là quên bỏ chế độ ghi khỏi vcr_config sau một buổi debug, tệ nhất là để all. Mọi lần chạy sẽ gọi API thật, CI bắt đầu cần token, và bạn quay về đúng chỗ xuất phát.

Kỹ năng này dùng thế nào ở site khách?

Ở một engagement thật, cassette còn là tài liệu. Khi khách hỏi hệ thống của bạn gửi gì sang API của họ, bạn mở file YAML cho họ xem request đã được che dữ liệu. Câu trả lời đó thuyết phục hơn mọi sơ đồ kiến trúc.

Nó cũng giúp bàn giao. Kỹ sư phía khách clone repo, chạy pytest --block-network và thấy test xanh mà không cần xin quyền truy cập sandbox. Bạn rời site, bộ test vẫn ở lại và vẫn chạy được.

Về nghề nghiệp: khi đọc JD cho vị trí FDE, hãy để ý những cụm như “integrate with customer systems” hay “own integrations end to end”. Trong CV, thay vì ghi “viết test”, hãy mô tả kết quả, ví dụ “dựng bộ test tích hợp offline cho API của khách, ghi bằng VCR.py, lọc credential và dữ liệu cá nhân, CI không cần mạng”.

Khi đi phỏng vấn, nếu bạn nói rõ được vì sao CI nên chạy ở chế độ none, người đối diện sẽ thấy bạn hiểu rủi ro, chứ không chỉ biết dùng một thư viện.

Lần tới sandbox của khách trả 503, CI của bạn sẽ không bị ảnh hưởng. Bạn chỉ cần gọi lại API thật vào hôm khách thông báo đổi schema.

Bài này có hữu ích không?

Dùng cùng trợ lý AIHỏi Claude ↗Hỏi ChatGPT ↗
5 nguồn
Đọc tiếp trên lộ trình · Chặng 1: Nền tảngVì sao Promise.all có thể làm hỏng cả buổi đồng bộ dữ liệu của khách hàng, và cách viết lạiChỉ cần một bản ghi lỗi là cả lô bị báo thất bại, trong khi các lệnh ghi vào hệ thống của khách vẫn tiếp tục chạy mà không ai còn theo dõi.