Biết dựng TransactionCase và điều khiển nó bằng @tagged rồi, giờ tới phần dễ làm ẩu nhất: bên trong test viết gì. Một test xanh chưa chắc là test tốt — tệ nhất là test trông như kiểm tra gì đó nhưng thật ra không khẳng định điều gì, nên nó xanh cả khi code đã hỏng. Bài này nói cách dựng dữ liệu gọn, chọn đúng phép assert, và tránh những cái bẫy "xanh giả".

Dựng dữ liệu: create, setUpClass, ref, new

Có mấy cách đưa dữ liệu vào test, chọn theo nhu cầu:

  • self.env['model'].create({...}) — cách chính, tạo bản ghi thật trong giao dịch test (sẽ cuộn ngược sau).
  • setUpClass thay vì setUp — dựng dữ liệu một lần cho cả lớp thay vì mỗi test, nhanh hơn khi dữ liệu nền dùng chung.
  • self.env.ref('module.xmlid') — lấy bản ghi đã có sẵn từ file XML (nhóm quyền, dữ liệu mẫu).
  • .new({...}) — tạo bản ghi ảo trong bộ nhớ, không ghi CSDL — hữu ích khi chỉ cần thử onchange/compute mà không cần lưu.

Ảnh chụp mã Python nền tối file test_data.py. Decorator tagged post_install trừ at_install. Class TestDuLieuVaAssert kế thừa TransactionCase. classmethod setUpClass gọi super rồi gán cls The là env quan the thanh vien, tạo cls bac name T-BAC hang bac diem 50 và cls vang name T-VANG hang vang diem 200 dựng một lần cho cả lớp. Method test_assert_record_values dùng assertRecordValues so bac cộng vang với danh sách hai dict name hang_the diem so nhiều field nhiều record trong một lời gọi. Method test_assert_raises_diem_am dùng with assertRaises ValidationError khi gán bac diem âm 1. Method test_subtest_nhieu_case lặp danh sách cặp trước sau 50 70, 200 220, 0 20 với subTest tạo thẻ gọi action_gia_han rồi assertEqual case hỏng không chặn case kia

Hình 1: setUpClass dựng hai thẻ mẫu một lần. Ba kiểu assert: assertRecordValues so nhiều field của nhiều bản ghi cùng lúc; assertRaises khẳng định thao tác ném đúng ngoại lệ; subTest lặp nhiều case trong một test — nếu case (50→70) hỏng, các case còn lại vẫn chạy, và log chỉ ra đúng case nào sai.

assertRecordValues: người bạn thân của Odoo

Đây là phép assert đặc trưng của Odoo mà unittest thuần không có. Thay vì viết mười dòng assertEqual cho mười field, assertRecordValues(recordset, [{...}, {...}]) so một lượt nhiều field của nhiều bản ghi — thứ tự dict khớp thứ tự bản ghi. Khi lệch, nó in ra đúng field nào của đúng bản ghi nào sai, gọn hơn hẳn.

Bẫy "xanh giả" — và bằng chứng assert báo lỗi rõ

Đây là phần quan trọng nhất. Vài kiểu test xanh mà vô dụng:

  • assertTrue(recordset_rỗng): một recordset rỗng là falsy, nên self.env['model'] (không search) làm assertTrue thất bại — hoặc tệ hơn, bạn tưởng nó kiểm "model tồn tại" mà thật ra đang kiểm "có bản ghi". (Đã vấp đúng bẫy này ở bài trước.)
  • Quên flush: ràng buộc CSDL chỉ kích hoạt khi dữ liệu được đẩy xuống DB; kiểm ngay sau khi gán mà chưa flush thì ràng buộc chưa nổ, test "xanh" nhầm.
  • Test không có assert nào: chạy code rồi kết thúc — luôn xanh, chẳng kiểm gì.
  • Dùng == thay assertEqual: self.bac.diem == 70 là một biểu thức bị vứt đi, không khẳng định gì. Phải assertEqual — và đây là lý do:

Ảnh chụp terminal nền tối hai phần chạy test thật. Phần 1 cố ý sai để xem assertEqual báo lỗi. Dòng ERROR test_data FAIL TestDuLieuVaAssert test_gia_han_SAI_CO_Y, dòng self assertEqual self bac diem 999, dòng AssertionError 70 khác 999 tô đỏ chú thích bac 50 cộng 20 bằng 70 không phải 999, dòng ERROR result 1 failed 0 errors of 4 tests. Phần 2 sửa kỳ vọng về 70 tất cả xanh. Bốn dòng Starting test_assert_raises_diem_am, test_assert_record_values, test_gia_han_dung, test_subtest_nhieu_case. Dòng stats 6 tests 0.04 giây 105 queries. Dòng result 0 failed 0 errors of 4 tests màu xanh

Hình 2: Thật. Ở phần 1, mình cố ý viết assertEqual(self.bac.diem, 999). Test hỏng với thông báo AssertionError: 70 != 999 — nói ngay giá trị thật (70) so với kỳ vọng (999). Đây là thứ == không bao giờ cho bạn: assertEqual in giá trị thực tế khi sai, giúp gỡ lỗi trong vài giây. Phần 2: sửa kỳ vọng về đúng 70 → cả 4 test xanh.

Chọn phép assert cho đúng

self.assertEqual(a, b)            # a phải bằng b (in cả hai khi sai)
self.assertTrue(x) / assertFalse(x)   # x đúng/sai — CẨN THẬN recordset rỗng
self.assertIn(x, tap)            # x nằm trong tập/chuỗi
with self.assertRaises(ValidationError):   # khối lệnh phải NÉM lỗi
    ...
self.assertRecordValues(rs, [{...}])   # so nhiều field nhiều record (Odoo)

Mỗi phép in một thông báo lỗi khác nhau khi hỏng; chọn đúng phép làm log dễ đọc. assertEqual cho giá trị hai vế, assertRaises cho biết lỗi mong đợi có nổ không, assertRecordValues chỉ ra field lệch.

Ba ý mang về

  1. Dựng dữ liệu gọn: create cho bản ghi thật, setUpClass dựng một lần cho cả lớp, env.ref lấy dữ liệu XML sẵn, .new cho bản ghi ảo không lưu DB.
  2. Chọn đúng phép assert: assertRecordValues so nhiều field nhiều record một lượt (đặc trưng Odoo), assertRaises cho đường thất bại, subTest để một case hỏng không chặn case khác.
  3. Tránh "xanh giả": đừng assertTrue(recordset rỗng), đừng quên flush trước khi kiểm ràng buộc DB, đừng viết test không có assert, và luôn dùng assertEqual chứ không == — đã thấy assertEqual in 70 != 999 rõ ràng, còn == thì im lặng.

Test đã vững, phần còn lại của sê-ri chuyển sang những mảng "vận hành" của một module chuyên nghiệp. Phần sau bàn về đa ngôn ngữ (i18n) — cách Odoo tách chuỗi dịch ra file .pot, dịch sang .po, để module của bạn nói được tiếng Việt lẫn tiếng Anh mà không sửa code.