Một trường related có thể biến 3 truy vấn thành 47 mà không bài test nào nhận ra. assertQueryCount, @warmup và @users dựng một cổng chặn hồi quy hiệu năng cho module Odoo ra sao.
Hãy hình dung bạn được giao thêm một trường related trỏ từ dòng đơn bán hàng sang quốc gia của đối tác. Trường này chỉ đọc, code sạch, unit test đều xanh. Sau khi đưa lên production, trang danh sách đơn bán hàng đi từ 3 truy vấn lên 47, vì ORM giờ gọi database theo từng dòng để lấy bản ghi đối tác, rồi lấy bản ghi quốc gia, cho mỗi dòng. ORM của Odoo sẽ không cảnh báo bạn và bộ test tiêu chuẩn cũng không bắt được, nhưng khách hàng của bạn thì có.
Phần lớn lập trình viên Odoo thường bỏ qua lớp kiểm thử hiệu năng vốn có sẵn trong framework. Bài này nói về assertQueryCount, @warmup, @users, @tagged, và một ví dụ thực tế đầy đủ từ module fs_image của OCA.
assertQueryCount
assertQueryCount(n) là một context manager trên odoo.tests.common.BaseCase. Bọc một khối bất kỳ trong nó và test sẽ hỏng nếu số câu SQL vượt quá n.
with self.assertQueryCount(42):
do_something()
Bên dưới, nó đọc cr.sql_log_count trước và sau khối lệnh, có flush ở cả hai đầu để bắt được các thao tác ghi bị ORM hoãn lại. Phần flush này quan trọng: Odoo gom một số thao tác ghi thành lô, và không có nó thì con số đếm ngay trước khi context manager kết thúc có thể bị thấp hơn thực tế. Con số bạn đem ra khẳng định là con số thật, không phải một con số lạc quan.
assertQueryCount cũng nhận tham số theo từng login để dùng chung với decorator @users:
with self.assertQueryCount(admin=3, demo=5):
do_something()
Cách này cho phép bạn ghi rõ và khẳng định các con số khác nhau cho từng loại người dùng, rất hữu ích khi phần kiểm tra quyền truy cập hoặc việc đánh giá ir.rule thay đổi theo người dùng.
@warmup
Lần đầu bất kỳ thao tác ORM nào chạy trong một test, nó nạp đầy các cache: định nghĩa trường, quyền truy cập, bản ghi ir.rule. Những lần trượt cache đó sinh ra truy vấn phụ. Con số ở lần chạy đầu bị thổi phồng và không ổn định.
@warmup giải quyết chuyện này bằng cách chạy test hai lần. Lượt đầu (self.warm = False) chạy trọn thân test, nạp đầy cache, rồi rollback. Lượt thứ hai (self.warm = True) chạy thật với các khẳng định được bật. assertQueryCount lặng lẽ bỏ qua trong lượt làm nóng, nên chỉ lượt thứ hai mới cho ra kết quả hỏng hay đạt.
@warmup
def test_my_operation(self):
with self.assertQueryCount(5):
self.env['sale.order'].browse(ids).action_confirm()
Không có @warmup, một test có thể đạt khi chạy trên máy phát triển (nơi cache đã nóng sẵn từ các lần chạy test trước hoặc từ thao tác trên giao diện) và hỏng trên CI (nơi mỗi lớp test khởi đầu từ trạng thái nguội). Hoặc ngược lại. Kiểu nào thì phép khẳng định cũng đang đo nhiễu. @warmup là bắt buộc với mọi assertQueryCount muốn có ý nghĩa.
Ghép lại: fs_image của OCA
Một ví dụ cụ thể là bài test của module storage fs_image thuộc OCA.
@users("__system__")
@warmup
def test_generated_sql_commands(self):
with self.assertQueryCount(__system__=3):
instance = self.env["test.image.model"].create(
{"fs_image": FSImageValue(name=self.filename, value=self.image_w)}
)
instance.invalidate_recordset()
with self.assertQueryCount(__system__=1):
self.assertEqual(instance.fs_image.getvalue(), self.image_w)
self.env.flush_all()
Đi từng dòng:
@users("__system__") chạy test dưới danh nghĩa người dùng hệ thống và truyền __system__ làm khóa login trong assertQueryCount. Hãy dùng @users khi con số đếm phụ thuộc vào người dùng đang hoạt động, và dùng dạng theo login của phép khẳng định cho khớp.
@warmup bảo đảm cache đã được nạp đầy trước khi đo. Luôn xếp nó vào bên trong @users (decorator trong cùng chạy trước).
assertQueryCount(__system__=3) khẳng định thao tác create tốn đúng 3 truy vấn khi chạy dưới người dùng hệ thống. Đây là phép khẳng định cho đường ghi.
instance.invalidate_recordset() xóa cache ORM theo bản ghi giữa hai phép khẳng định. Không có dòng này, lần đọc ở khối thứ hai có thể trúng cache trong bộ nhớ thay vì xuống database, làm con số truy vấn thật bị hạ thấp.
assertQueryCount(__system__=1) khẳng định đường đọc tốn 1 truy vấn. Hai phép khẳng định tách bạch, ghi và đọc, cho một ngân sách chính xác cho từng thao tác.
Khuôn mẫu này chép lại dùng ngay được. Thay model, thay các giá trị create, thay con số ngưỡng bằng của bạn, thế là bạn có một cổng chặn hồi quy hiệu năng cho bất kỳ module Odoo nào.
Đi xa hơn: Profiler
Khi assertQueryCount cho bạn biết con số quá cao nhưng không cho biết vì sao, hãy dùng Profiler. Đó là một context manager bắt lại các câu SQL, các vết ngăn xếp lấy theo chu kỳ, và các ảnh chụp bộ nhớ, tất cả trong một lượt chạy.
with self.profile():
self.env['sale.order'].browse(ids).action_confirm()
Kết quả được lưu vào database và xem được trên giao diện hiệu năng của Odoo. Hãy dùng Profiler khi cần lần ra phương thức nào đang sinh thêm truy vấn, chứ không chỉ biết là tổng số đã vượt ngân sách.
Kết luận
Kiểm thử hiệu năng trong Odoo thường bị bỏ qua trong lúc phát triển, và chỉ được phát hiện là còn thiếu khi người dùng thật bắt đầu dùng module trên production. Tới lúc đó, cái giá là một ticket hỗ trợ, một lần rollback, và niềm tin của khách hàng.
assertQueryCount cộng @warmup là tấm lưới an toàn tối thiểu. Thêm @users khi con số thay đổi theo người dùng. Dùng Profiler khi cần lần ra gốc rễ.