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

Sách & khoá học

pytest của Brian Okken: viết bộ test mà khách dám chạy sau khi bạn rời đi

Bạn bàn giao xong rồi rời đi, nhưng connector thì ở lại, và đội của khách chỉ dám sửa nó khi bộ test đủ rõ để họ tin.

Bìa sách Python Testing with pytest
Python Testing with pytest · Brian Okken · Ảnh bìa: Open Library
Chia sẻLinkedInFacebookX

Tóm tắt nhanh

  • Sách của Brian Okken, Pragmatic Bookshelf phát hành tháng 2/2022, dày 272 trang. Theo tác giả, ấn bản hai tăng từ 7 lên 17 chương.
  • Ba thói quen đáng giữ nhất: dồn setup vào fixture, cho một test chạy nhiều case bằng parametrize, và chạy test bằng tox trên CI.
  • Rất hợp với FDE phải bàn giao code tích hợp: nên đọc chương fixture và parametrize trước, mock và CI đọc sau.
Cây quyết định chọn scope cho pytest fixture. Nếu test ghi hoặc sửa dữ liệu của fixture thì dùng scope="function" và dọn sạch sau yield. Nếu không, như kết nối hay client, thì có thể dùng scope="module".
Thứ tốn kém mà test chỉ đọc thì để scope rộng. Thứ test sẽ ghi hoặc sửa thì để scope function và dọn sạch sau yield.

Một engagement kết thúc thì bạn rời đi, còn code tích hợp vẫn ở lại. Vài tháng sau, có thể một kỹ sư bên khách hàng mở repo, sửa một dòng, rồi chạy test. Họ có dám merge hay không phụ thuộc vào việc bộ test bạn để lại có dễ đọc không và đáng tin đến đâu.

Vì thế một cuốn sách về công cụ test vẫn đáng để FDE bỏ thời gian đọc. Python Testing with pytest ấn bản thứ hai của Brian Okken do Pragmatic Bookshelf phát hành tháng 2/2022, dày 272 trang. Với FDE, cái được lớn nhất từ cuốn sách là học cách viết bộ test mà người đến sau đọc là hiểu.

Cuốn sách dạy gì, và dạy theo cách nào?

Nhà xuất bản gọi đây là bản sửa toàn diện, gồm assert, fixture, parametrize, marker và plugin, và cuốn sách được xây quanh một ứng dụng cơ sở dữ liệu nhỏ.

Okken tự giới thiệu sách là tutorial về pytest “nhẹ nhàng và đầy đủ nhất”, viết để người đọc học dần từng bước. Theo lời ông, ấn bản hai tăng từ 7 lên 17 chương. Parametrize được tách thành chương riêng, còn coverage, mock và CI được viết sâu hơn.

Mục lục có một chương riêng về mock, gồm mock thuộc tính, class, method và các plugin hỗ trợ. Một chương khác nói về tox và CI, trong đó có phần chạy tox bằng GitHub Actions.

Điều Okken muốn chứng minh là nhờ fixture, pytest dùng được cho cả test nhỏ lẫn functional test phức tạp. Với FDE, như vậy là không cần hai bộ công cụ riêng cho unit test và test tích hợp.

Ý tưởng thứ nhất: setup hỏng không phải test hỏng

Lời khuyên giá trị nhất trong sách có lẽ là đưa code setup ra khỏi thân test và đặt vào fixture, để khi setup lỗi thì phân biệt được với test lỗi. Thử hình dung bạn bàn giao một connector đồng bộ đơn hàng từ API của khách hàng vào kho dữ liệu.

Nếu test tự mở kết nối và tự tạo bảng tạm, thì lúc token hết hạn, mọi test đều báo FAILED. Người kế nhiệm sẽ đi tìm bug trong logic mapping, trong khi logic đó chẳng sai gì.

Chuyển phần đó sang fixture thì pytest báo ERROR ở bước setup thay vì FAILED ở assert. Chỉ cần nhìn báo cáo là biết lỗi do môi trường hay do code. Theo tài liệu chính thức của pytest, với yield fixture thì mọi code teardown đặt sau yield:

@pytest.fixture(scope="module")
def staging_db():
    conn = connect_staging()   # setup
    yield conn
    conn.drop_temp_tables()    # teardown
    conn.close()

Chương fixture còn có phần về scope và cách chia sẻ fixture qua conftest.py. Khi bàn giao, một file conftest.py gọn gàng có giá trị như một trang tài liệu: mọi thứ test cần để chạy đều nằm ở một chỗ.

Ý tưởng thứ hai: một test, nhiều dữ liệu thật

Sách trình bày parametrize là cách dùng một test để chạy nhiều test case. Tài liệu pytest xác nhận @pytest.mark.parametrize cho phép khai báo nhiều bộ đối số ngay tại hàm test. Chương này còn đi xa hơn decorator: nó bàn cả parametrize cho fixture và hook pytest_generate_tests.

Áp vào connector ở trên: dữ liệu khách hàng hiếm khi sạch. Thử hình dung cột ngày đặt hàng đến với ba định dạng khác nhau. Thay vì viết ba hàm test, bạn viết một:

@pytest.mark.parametrize("raw_date, expected", [
    ("2024-03-01", date(2024, 3, 1)),
    ("01/03/2024", date(2024, 3, 1)),
    ("1 Mar 2024", date(2024, 3, 1)),
])
def test_parse_order_date(raw_date, expected):
    assert parse_order_date(raw_date) == expected

Pytest sẽ báo ba kết quả riêng, nên nếu chỉ định dạng thứ hai hỏng thì báo cáo chỉ ra đúng dòng đó. Mỗi lần gặp một kiểu dữ liệu xấu mới ngoài thực tế, bạn thêm một dòng vào danh sách. Đến lúc bàn giao, danh sách đó trở thành bản ghi lại mọi kiểu dữ liệu xấu bạn đã gặp ở khách hàng này.

Parametrize fixture hữu ích khi cùng một logic phải chạy trên hai backend, chẳng hạn staging và một bản sao cục bộ. Bạn viết test một lần, rồi để fixture quyết định test chạy trên môi trường nào.

Hai cái bẫy khi mới dùng fixture

Bẫy đầu tiên là biến conftest.py thành chỗ chứa mọi thứ. Fixture nào chỉ một file test dùng thì nên đặt ngay trong file đó. Nếu conftest.py dài vài trăm dòng, người tiếp quản sẽ phải đọc hết mới biết test nào phụ thuộc vào cái gì, và nó mất luôn vai trò trang tài liệu.

Bẫy thứ hai là chọn sai scope. Fixture staging_db ở trên để scope="module" là hợp lý, vì mở kết nối tốn thời gian. Nhưng nếu fixture tạo dữ liệu mẫu mà test sẽ sửa cũng để scope rộng như vậy, thì test chạy trước để lại dữ liệu cho test chạy sau, và bộ test lúc xanh lúc đỏ tùy thứ tự chạy.

Có một cách chia đơn giản: thứ gì tốn kém và chỉ đọc (kết nối, client) thì để scope rộng, thứ gì test sẽ ghi hoặc sửa thì để scope function và dọn sạch sau yield. Không dọn thì bảng tạm sẽ chất dần trên staging của khách, và người phải dọn sẽ là đội của họ.

Ý tưởng thứ ba: test không chạy tự động thì sớm muộn sẽ hỏng

Sách còn hướng dẫn mở rộng pytest bằng plugin, nối vào hệ thống CI, dùng chung với tox, mock và coverage, và chạy cùng các test unittest có sẵn. Chi tiết cuối nghe nhỏ nhưng rất có ích: nếu repo của khách đã có test viết bằng unittest, bạn vẫn chạy được pytest mà không phải viết lại từ đầu.

Khi không được gọi thẳng vào hệ thống production của khách trong lúc test, bạn sẽ cần đến mock. Còn tox chạy trên GitHub Actions giúp bộ test vẫn được chạy mỗi khi có người push code, kể cả khi bạn đã rời dự án từ lâu.

Ai nên đọc, và đọc theo thứ tự nào?

Nếu bạn đã viết Python nhưng test vẫn là mấy hàm assert rời rạc, hãy đọc theo thứ tự: các chương đầu về assert, rồi fixture, rồi parametrize. Mock, tox và CI để dành đến khi bạn có một repo thật cần bàn giao.

Nếu đã dùng pytest thành thạo, bạn có thể đọc thẳng phần fixture scope, conftest.py và pytest_generate_tests. Đó là chỗ phân biệt người chỉ biết dùng pytest với người dựng được cả một bộ test.

Về nghề nghiệp, câu “có viết test” trong CV không gây ấn tượng gì. Một câu như “bàn giao connector kèm bộ test pytest có fixture cho môi trường staging, chạy tự động bằng tox trên GitHub Actions” thì khác: nó cho thấy bạn biết nghĩ cho người tiếp quản sau mình.

Nếu mô tả công việc FDE bạn nhắm tới có nhắc đến bàn giao hay bảo trì code cho khách, hãy chuẩn bị sẵn ví dụ này để kể khi phỏng vấn.

Sách phát hành từ năm 2022, nhưng điều nó dạy chưa hề cũ. Bộ test tốt giúp đội của khách tự tin sửa code sau khi bạn đã rời đi.

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

Dùng cùng trợ lý AIHỏi Claude ↗Hỏi ChatGPT ↗
4 nguồn
Đọc tiếp trên lộ trình · Chặng 2: Kỹ thuật rộngDesigning Web APIs: đọc API của khách bằng con mắt người đã thiết kế ra nóSách viết cho người xây API, nhưng người được lợi nhiều lại là người phải tích hợp với API đó.