Tình trạng ảm đạm của các thư viện kiểm thử dựa trên thuộc tính
(stevana.github.io)- Property-based testing đã lan rộng sang nhiều ngôn ngữ sau QuickCheck, nhưng tính đến tháng 7/2024, nhiều thư viện vẫn chưa cung cấp đầy đủ kiểm thử dựa trên trạng thái và kiểm thử song song, vốn đã được tổng kết từ năm 2009
- Khoảng cách cốt lõi nằm ở khả năng dùng mô hình máy trạng thái để kiểm chứng các thay đổi trạng thái tuần tự, rồi tái sử dụng cùng mô hình đó cho kiểm tra khả năng tuyến tính hóa (linearisability) nhằm tìm race condition trong thực thi song song
- Nhiều thư viện được khảo sát không có kiểm thử dựa trên trạng thái hoặc chỉ ở mức thử nghiệm; kiểm thử song song còn hiếm hơn, với các issue liên quan vẫn tồn tại nhiều năm trong FsCheck, Gopter, RapidCheck, SwiftCheck, jsverify, v.v.
- Một bản triển khai Haskell khoảng 400 dòng tái hiện property-based testing dựa trên trạng thái và song song, đồng thời dùng triển khai tham chiếu dựa trên fake quen thuộc với lập trình viên làm mô hình thay cho đặc tả máy trạng thái truyền thống
- Fake đã được kiểm thử hợp đồng có thể được tái sử dụng không chỉ để kiểm chứng một component đơn lẻ, mà còn cho kiểm thử tích hợp nhanh và tất định bằng cách tiêm vào thay cho dependency thật
Khoảng cách tính năng xuất hiện sau QuickCheck
- Property-based testing lan rộng sang các cộng đồng ngôn ngữ lập trình khác nhau dưới khẩu hiệu “đừng viết test, hãy sinh test”
- Trang Wikipedia của QuickCheck, thư viện Haskell gốc, liệt kê 57 bản triển khai lại cho các ngôn ngữ khác
- Bài báo QuickCheck đầu tiên, QuickCheck: A Lightweight Tool for Random Testing of Haskell Programs, được trình bày tại ICFP 2000; toàn bộ mã nguồn của bản triển khai đầu tiên chỉ khoảng 300 dòng trong phụ lục bài báo
- QuickCheck ban đầu chỉ có thể kiểm thử hàm thuần, và đến năm 2002, Testing monadic code with QuickCheck đặt nền tảng để xử lý code có hiệu ứng như mutable state, file I/O và networking
Sự xuất hiện của kiểm thử dựa trên trạng thái và song song
- Quviq AB được John Hughes và Thomas Arts thành lập năm 2006, và việc kiểm thử các dự án Erlang của Ericsson là một trong những use case ban đầu
- Erlang không phải là ngôn ngữ hàm thuần và concurrency rất phổ biến, nên chỉ dùng QuickCheck monadic hiện có là chưa đủ tiện lợi
- Erlang QuickCheck closed source của Quviq sau đó bao gồm hai tính năng vắng mặt trong nhiều bản triển khai open source
- Property-based testing tuần tự dựa trên trạng thái dùng mô hình máy trạng thái
- Kiểm thử song song tái sử dụng cùng mô hình máy trạng thái tuần tự để phát hiện race condition
- Kiểm thử dựa trên trạng thái xuất hiện với hình thức hiện nay trong QuickCheck testing for fun and profit (2007)
- Kiểm thử song song được trình bày chi tiết trong Finding Race Conditions in Erlang with QuickCheck and PULSE (ICFP 2009), lấy Linearizability: a correctness condition for concurrent objects (1990) của Herlihy và Wing làm kỹ thuật cốt lõi
- Mã thư viện của Quviq QuickCheck không được chia sẻ trong các bài báo; những gì được công khai là API và các ví dụ test sử dụng API đó
Kết quả khảo sát thư viện năm 2024
- State-of-the-art hiện nay là stateful testing dựa trên mô hình máy trạng thái và parallel testing kết hợp linearisability với cùng mô hình tuần tự đó
- Khảo sát được tổng hợp dựa trên việc đọc tài liệu, issue tracker và một phần mã nguồn tính đến tháng 7/2024
- Nhiều thư viện không cung cấp kiểm thử dựa trên trạng thái hoặc chỉ cung cấp ở mức hạn chế
- QuickCheck(Haskell) có issue bổ sung kiểm thử dựa trên trạng thái mở từ năm 2016
- SwiftCheck cũng có issue bổ sung kiểm thử dựa trên trạng thái mở từ năm 2016
- jsverify vẫn còn issue bổ sung kiểm thử dựa trên trạng thái từ năm 2015
- proptest(Rust) cần tham khảo gói riêng proptest-state-machine
- Hỗ trợ kiểm thử song song còn hiếm hơn
- Gopter ghi trong README rằng “No parallel commands … yet?” và có issue từ năm 2017
- FsCheck có issue bổ sung parallel support mở từ năm 2016
- RapidCheck có issue bổ sung parallel support mở từ năm 2015
- propcheck có issue bổ sung parallel testing từ năm 2020
- Các ví dụ open source hỗ trợ cả hai tính năng gồm PropEr, Hedgehog, qcheck-stm, quickcheck-state-machine, stateful-check, v.v.
- Ngay cả khi có tính năng song song, vẫn có trường hợp bị hạn chế
- Chú thích trong mã nguồn của QuickTheories ghi rằng kiểm thử song song của thư viện này khiến số end state khả dĩ tăng nhanh theo số lệnh, nên thường phải giới hạn command list ở mức 10 lệnh trở xuống
- Các ví dụ LevelDB và Redis của ScalaCheck được trình bày là ví dụ tuần tự với
threadCount = 1 - Hỗ trợ race condition của fast-check có vẻ không tái sử dụng mô hình máy trạng thái tuần tự hoặc dùng linearisability như kiểm thử song song của Quviq QuickCheck
- Không thấy ví dụ rõ ràng nào về việc tính năng song song được thêm vào sau; nếu không được phản ánh trong thiết kế API ngay từ đầu, có thể cần tái thiết kế đáng kể
Vì sao tính năng lan rộng chậm
- John Hughes đưa ra ba lý do
- Kiểm thử dựa trên trạng thái và song song không hữu ích bằng kiểm thử hàm thuần
- Viết mô hình máy trạng thái đòi hỏi cách tư duy khác với test thông thường và cần đào tạo
- Chỉ open source thì khó được ngành công nghiệp chấp nhận, còn sản phẩm closed source cùng đào tạo và tư vấn đã hỗ trợ việc áp dụng
- Chỉ cần property-based testing cho các phần hàm thuần cũng đem lại nhiều hiệu quả, nhưng trong hệ thống công nghiệp có nhiều database, stateful protocol và concurrent data structure, nên kiểm thử dựa trên trạng thái và song song cũng gần như quan trọng tương đương
- Đặc tả dựa trên trạng thái không phải lúc nào cũng khó hơn đặc tả bằng hàm thuần
- Mô hình key-value store có thể đi khá xa chỉ với danh sách key-value pair
- Trong trường hợp LevelDB, một mô hình đơn giản đã tìm ra counterexample rút gọn 17 bước chỉ trong vài phút; sau khi Google sửa, nó lại tìm ra counterexample 31 bước trong vài phút
- Vấn đề thứ hai là lỗi trong background compaction process; compaction quan trọng cho việc cải thiện hiệu năng đọc và thu hồi disk space, nhưng không được đưa rõ vào mô hình
- Closed source có thể đã giúp việc áp dụng trong công nghiệp, nhưng được đánh giá là không giúp ích cho việc áp dụng open source
- Để tái hiện kết quả của các bài báo mà không có giấy phép Quviq QuickCheck, cần rất nhiều reverse engineering, nên gần như bất khả thi
Đề xuất: triển khai nhỏ, công khai và đặc tả dễ viết
- Có hai hướng cải thiện
- Cung cấp một bản triển khai open source ngắn cho property-based testing dựa trên trạng thái và song song, giống bản triển khai QuickCheck gốc khoảng 300 dòng
- Giảm gánh nặng viết đặc tả bằng cách tái sử dụng các khái niệm mock và test double mà lập trình viên đã quen thuộc, thay vì máy trạng thái
- Để kiểm chứng giả thuyết này, hai điều được trình bày
- Triển khai property-based testing dựa trên trạng thái và song song bằng khoảng 400 dòng code
- Dùng in-memory reference implementation, tức fake, làm mô hình thay cho state machine
Tóm tắt kiểm thử dựa trên thuộc tính thuần túy
- Trong kiểm thử hàm thuần túy, ta sinh đầu vào và kiểm tra xem đầu ra của hàm có thỏa mãn một quan hệ nào đó với đầu vào hay không
- Ví dụ,
reversecó thể được kiểm thử bằng thuộc tínhreverse (reverse xs) == xsvới danh sách bất kỳxs - QuickCheck mặc định sinh 100 ca kiểm thử; nếu thất bại, nó shrink đầu vào để đưa ra counterexample nhỏ nhất
- Một thuộc tính sai như
reverse xs == xssẽ được thu nhỏ thành phản ví dụ tối thiểu như[0,1] - Các mẫu thuộc tính thường gặp gồm inverse, idempotency, associativity, các axiom của abstract data type, metamorphic property, v.v.
- inverse:
deserialise (serialise i) == i - idempotency:
sort (sort xs) == sort xs - associativity:
(i + j) + k == i + (j + k)
- inverse:
Kiểm thử dựa trên thuộc tính theo trạng thái
- Các thành phần có trạng thái không phải lúc nào cũng tạo cùng một đầu ra từ cùng một đầu vào
- Kết quả
incrlần đầu và kết quảincrlần thứ hai của counter phụ thuộc vào trạng thái trước đó - database và file system cũng có lịch sử đầu vào trước đó ảnh hưởng đến đầu ra tiếp theo
- Kết quả
- Nếu kiểm thử hàm thuần túy xử lý một đầu vào đơn lẻ, thì kiểm thử theo trạng thái sinh chuỗi đầu vào để kiểm tra hệ thống thay đổi theo thời gian như thế nào
- Model được biểu diễn dưới dạng fake có dạng
m -> i -> (m, o)- Từ trạng thái model trước đó
mvà đầu vàoi, tính model tiếp theo và đầu rao - So sánh đầu ra của hệ thống thực với đầu ra của fake ở từng bước
- Nếu không khớp, shrink chuỗi đầu vào để tìm counterexample nhỏ
- Từ trạng thái model trước đó
-
Ví dụ Counter
- Lấy một counter Haskell dùng biến mutable toàn cục làm đối tượng kiểm thử
incrtăng counter, còngetđọc giá trị hiện tại- Model chỉ cần một
Counter Int; instanceStateModelđịnh nghĩa trạng thái ban đầuCounter 0,Incr,Get,Incr_ (),Get_ Int,runFake,runReal, và command generator - Nếu đưa vào lỗi như
incr42Bug, trong đó counter không tăng khi giá trị là 42, QuickCheck tìm thấy thất bại sau 66 ca kiểm thử và shrink 29 lần để đưa ra phản ví dụ tối thiểu: increment 43 lần rồiGet - Nếu không
resetcounter toàn cục thực giữa các ca kiểm thử, model luôn bắt đầu từ 0 nhưng counter thực giữ trạng thái từ ca kiểm thử trước, dẫn đến mismatch
-
Giao diện thư viện theo trạng thái
- Giao diện
StateModelxem hệ thống cần kiểm thử như một black box, coi command là đầu vào và response là đầu ra - Các thành phần cốt lõi gồm
Command state,Response state,initialState,runFake,runReal,generateCommand - Các thành phần tùy chọn như sau
Reference: dùng khi command sau cần tham chiếu đến tài nguyên do response trước tạo ra, chẳng hạn file handlePreconditionFailure: biểu diễn lỗi precondition kiểu không cho phép read từ handle không phải file đang mởCommandMonad: mặc định làIO, nhưng có thể dùng monad khácmonitoring,commandName: dùng cho coverage và thống kê
- Khi sinh command, không thể tạo các giá trị như file handle thực, nên sinh symbolic reference dạng
Var Intvà thay thế bằng reference thực trong lúc chạy - Sau khi shrink, loại bỏ các command phá vỡ precondition hoặc dùng symbolic reference ngoài scope
- Giao diện
-
Ví dụ circular buffer
- Kiểm thử một circular queue viết bằng C thông qua Haskell FFI, còn model được viết dưới dạng queue đơn giản dựa trên danh sách
- Phần triển khai C không kiểm tra lỗi, nên nếu
gettừ empty queue thì có thể trả về bộ nhớ chưa được khởi tạo - Triển khai thực hiệu quả nhờ circular index nhưng không hiển nhiên là correct, còn fake kém hiệu quả hơn nhưng chỉ dùng để kiểm thử nên không thành vấn đề
- Vì
newtrả về queue reference, model quản lý nhiều queue bằngMap (Var Queue) FQueue - Ban đầu thiếu precondition cho việc
putvào full queue, nên khi đưa0,1vào queue kích thước 1 rồiget, model kỳ vọng0theo FIFO nhưng mã C trả về1 - Đây không phải lỗi triển khai mà là thiếu precondition trong model, nên được sửa bằng cách thêm precondition
QueueIsFull - Việc command
Sizebị thiếu trong generator được phát hiện qua đầu ra coverage; sau khi thêm vào thì phát hiện lỗi tính kích thước queue - Với queue kích thước 1 chứa một item,
Sizecó giá trị kỳ vọng là 1 nhưng giá trị thực là 0; bản sửa được đề xuất là đặt kích thước buffer nội bộ thànhn + 1trongnew - Sau đó,
abs(q->inp - q->outp) % q->sizevượt qua với kích thước 1 nhưng lại thất bại với kích thước 2; bản sửa cuối cùng là(q->inp - q->outp + q->size) % q->size
-
Câu đố bình nước trong Die Hard 3
- Giải câu đố tạo đúng 4L bằng bình nước 3L và 5L bằng kiểm thử theo trạng thái
- Ngay cả khi không có triển khai thực và chỉ chạy model cùng fake, có thể làm bài kiểm thử thất bại khi đạt đến một trạng thái cụ thể để thu được action sequence đã được shrink
- Sau 199 ca kiểm thử và 11 lần shrink, sequence được đưa ra có luồng như sau
- Đổ đầy bình 5L
- Rót từ bình 5L sang bình 3L
- Đổ cạn bình 3L
- Rót lại từ bình 5L sang bình 3L
- Đổ đầy bình 5L
- Rót từ bình 5L sang bình 3L
- Trace hiển thị các trạng thái trung gian, cho phép xác nhận quá trình big jug đạt 4L
Kiểm thử dựa trên thuộc tính song song
- Lỗi trong concurrent code rất khó tái hiện và xác minh việc sửa lỗi vì thread interleaving thay đổi qua mỗi lần chạy
- Mục tiêu là cho phép người dùng thực hiện kiểm thử song song giống như kiểm thử dựa trên trạng thái tuần tự, mà không phải viết thêm nhiều mã kiểm thử
- Trong ví dụ counter, nếu
incrthực hiệnreadIORefrồiwriteIOReftheo cách không nguyên tử, hai thread có thể ghi đè increment của nhau và gây ra race condition - Kiểm thử song song thu thập thời điểm invocation và response của các command trong lúc chạy để tạo concurrent history, rồi kiểm tra history đó có thể được giải thích bằng một interleaving tuần tự nào hay không
- Nếu có ít nhất một interleaving khớp với mô hình tuần tự, history được xem là linearise và được đánh giá là correct
- Nếu không có interleaving tuần tự nào giải thích được response thực tế, kết quả được xử lý là non-linearisable
-
Sinh và shrink command song song
- Chương trình song song được biểu diễn bằng
ParallelCommandsvà nhiềuFork, trong đó các command bên trong mỗiForkđược chạy song song - Phần triển khai ví dụ xử lý việc thực thi một, hai và ba thread
- Trong thực thi song song, trạng thái mô hình khả dĩ có thể thay đổi tùy theo interleaving, chẳng hạn
Fork [Write "a" "foo", Write "a" "bar"] - Mô hình song song thực hiện sinh command và shrink dựa trên tập trạng thái, chứ không phải một state đơn lẻ
parallelSafekiểm tra xem precondition có được duy trì trong mọi permutation của các command bên trongForkhay không- Ví dụ, nếu
Write "a"vàDelete "a"nằm trong cùng một fork, một command có thể phá vỡ precondition của command kia - Trong quá trình shrink, chỉ giữ lại các command vẫn duy trì precondition và phạm vi symbolic reference
- Chương trình song song được biểu diễn bằng
-
Thực thi song song và kiểm tra linearisability
- Thực thi song song ghi lại event
InvokevàOkcủa mỗi command vào history - Nếu response chứa reference mới, môi trường được mở rộng bằng atomic counter để tránh xung đột số reference giữa các thread
- Tất cả interleaving khả dĩ từ history được liệt kê dưới dạng cây
Rose linearisablekiểm tra xem có path nào trong cây này làm response khớp với mô hình tuần tựrunFakehay không- Vì kiểm thử song song cuối cùng vẫn tái sử dụng mô hình tuần tự, người dùng chỉ cần viết mô hình tuần tự rồi thêm rất ít mã để có kiểm thử song song
- Thực thi song song ghi lại event
-
Ví dụ Parallel counter
- Mã được thêm để kích hoạt kiểm thử song song cho counter chỉ gồm instance
ParallelModel Countervà property - Khi dùng
incrRaceConditionkhông nguyên tử, kiểm thử tìm ra race condition - Ngay cả khi test case nhỏ hơn vẫn có race, nếu lỗi không tái hiện do một interleaving khác, QuickCheck có thể xem test case nhỏ hơn là pass và dừng shrink
- Giải pháp đúng là deterministic thread scheduler, và bài báo về kiểm thử song song sử dụng cách này
- Phần triển khai ví dụ dùng một workaround đơn giản hơn: chèn sleep ngắn quanh thao tác read/write trên shared memory để tăng khả năng xảy ra cùng một interleaving
- Sleep không cần để tìm race, mà để làm cho counterexample của race đã tìm được trở nên nhỏ hơn
- Sau khi thêm sleep, phản ví dụ tối thiểu được rút gọn thành
ParallelCommands [Fork [Incr,Incr],Fork [Get]]
- Mã được thêm để kích hoạt kiểm thử song song cho counter chỉ gồm instance
-
Ví dụ Process registry
- Hệ thống ví dụ tương tự Erlang process registry: spawn thread, rồi register·lookup·unregister·kill
ThreadIdtheo tên - Mô hình tuần tự theo dõi các thread id đã được tạo, các cặp name-thread đã đăng ký, và các thread id đã bị kill
RegistervàUnregistercó thể thất bại, nên response dùngEither ErrorCall ()- Thông tin error location của triển khai thực được loại bỏ bằng
abstractErrorđể khớp với fake monitoringhiển thị coverage choRegisterFailed,RegisterSucceeded,UnregisterFailed,UnregisterSucceeded- Nếu cố ý đưa vào lỗi khiến
registerghi đè registry hiện có, sẽ xuất hiện một phản ví dụ tuần tự trong đó không thể unregister"e"đã đăng ký trước đó - Trong kiểm thử song song, phản ví dụ dài hơn xuất hiện; khi dùng
SleepyIORef, nó shrink về dạngFork [Register "b" (Var 0), Register "c" (Var 0)] - Vấn đề là race trong khoảng giữa lúc kiểm tra bằng
readRegistryvà lúc gọiatomicModifyIORef, nơi một thread khác có thể chen vào - Sau khi áp dụng global lock cho
register,unregister,kill, kiểm thử song song pass
- Hệ thống ví dụ tương tự Erlang process registry: spawn thread, rồi register·lookup·unregister·kill
Mô hình dựa trên fake và kiểm thử tích hợp
- Thay vì state machine specification truyền thống với post-condition, dùng fake in-memory làm reference implementation
- Bài viết năm 2019 của Edsko de Vries năm 2019 được giới thiệu là bài đầu tiên đề xuất cách triển khai fake trên đặc tả state machine dựa trên post-condition
- fake tương tự mock, nên được trình bày như một cách tiếp cận dễ hơn cho các lập trình viên chưa quen với formal specification
- fake cũng có lợi thế là có thể dùng thay cho component phụ thuộc trong kiểm thử tích hợp
- Không cần khởi động hoặc enable dependency thật
- Có thể xây dựng integration test nhanh hơn và deterministic hơn
- Vấn đề fake có thể sai được xử lý bằng contract test
- Vì kiểm thử property-based dựa trên trạng thái và song song xác minh sự khớp nhau giữa fake và triển khai thật, fake đóng vai trò như một dependency đã được contract-test
-
Tách kiểm thử và triển khai bằng Queue fake
- Interface queue
IQueuecóiNew,iPut,iGet,iSize - Triển khai thật nối trực tiếp C queue wrapper
- Triển khai fake lưu trạng thái mô hình trong
IORefvà cập nhật quafNew,fPut,fGet,fSize - Component được viết theo interface
IQueue q - Khi kiểm thử dùng instance
fake, khi triển khai dùng instancereal - Dùng kiểm thử property-based dựa trên trạng thái để đặt tiền đề rằng fake faithful với real
- Interface queue
-
File system fake
- Interface file system
IFileSystem hcóiMkDir,iOpen,iWrite,iClose,iRead - Triển khai thật dùng file system thực dưới
/tmp/qc-test - fake được triển khai bằng
FakeFSin-memory gồm directory set, file content map, open handle map và next handle fOpen,fWrite,fClose,fReadmô hình hóa các precondition failure như busy file, directory không tồn tại, handle đã đóng- Nếu fake file system được kiểm thử là faithful với file system thật, các component phụ thuộc vào file system có thể integration test bằng fake và thay bằng real file system khi triển khai
- Nếu phát sinh bug khi thay bằng real, cần điều tra mismatch giữa fake và real đã vượt qua kiểm thử property-based dựa trên trạng thái như thế nào
- Interface file system
-
Hệ thống component lớn hơn
- Hệ thống trong đó A phụ thuộc vào B và B phụ thuộc vào C cũng được mở rộng theo cùng cách
- Đặt interface cho từng component
iC :: IO ICiB :: IC -> IO IBiA :: IB -> IO IA
- Chiến lược kiểm thử như sau
- Xác minh C bằng kiểm thử property-based dựa trên trạng thái và song song để thu được fake C đã contract-test
- Trong integration test của B, dùng fake C
- Trong kiểm thử A, dùng fake B sử dụng fake C
- Cách này mở rộng theo cùng một pattern cho nhiều component hoặc service hơn
Kết luận
- Kiểm thử property-based dựa trên trạng thái và song song có thể được triển khai bằng khoảng 400 dòng code, quy mô tương đương bản triển khai QuickCheck đầu tiên khoảng 300 dòng khi chưa có shrinking
- Dùng fake làm mô hình giúp việc viết đặc tả cho kiểm thử dựa trên trạng thái và song song trở nên quen thuộc hơn, đồng thời có thể tái sử dụng để kiểm thử các hệ thống lớn hơn theo cách compositional
- Nếu cộng đồng từng ngôn ngữ tiếp tục thử nghiệm, vẫn còn dư địa để cải thiện tình trạng của các thư viện property-based testing
1 bình luận
Ý kiến trên Hacker News
Fuzzing dựa trên coverage đã xuất hiện và Go cũng hỗ trợ khá tốt, nên tôi tò mò nếu không dùng thư viện kiểm thử dựa trên thuộc tính thì sẽ bỏ lỡ điều gì
https://www.tedinski.com/2018/12/11/fuzzing-and-property-tes...
Nhìn vào fuzz test bên dưới và phần kiểm tra bất biến tương ứng, tôi thấy về thực chất nó gần như giống kiểm thử thuộc tính
https://github.com/ncruces/aa/blob/505cbbf94973042cc7af4d6be...
https://github.com/ncruces/aa/blob/505cbbf94973042cc7af4d6be...
Có khác biệt thực sự, nhưng ranh giới khá mờ, và việc phân định chính xác cái gì là fuzzing, cái gì là kiểm thử dựa trên thuộc tính không quá quan trọng
Các bài test chạy nhanh với assertion chi tiết thì là kiểm thử dựa trên thuộc tính; chạy lâu và chỉ tìm crash thì là fuzzing; phần ở giữa thì mơ hồ
https://hypothesis.works/articles/what-is-property-based-tes...
Khi còn ở Google, có một công cụ nội bộ kết hợp hai thứ này rất tốt. Bạn viết kiểm thử dựa trên thuộc tính như bình thường; khi chạy, test framework sẽ biên dịch theo cách đặc biệt để thu được coverage và điều chỉnh input ngẫu nhiên nhằm tăng coverage. Tất nhiên nó chạy hoàn toàn tự động trên một cụm nhiều máy
Kiểm thử dựa trên thuộc tính truyền thống thường chỉ được triển khai dưới dạng thư viện, nên không nhất thiết có thông tin coverage để dẫn hướng việc sinh input ngẫu nhiên
Tuy nhiên tùy thư viện, bạn có thể nhận được khá nhiều tiện ích. Một trong những thứ hữu ích là shrinking, có thể tham khảo mục “Shrinking” ở đây: https://tech.fpcomplete.com/blog/quickcheck-hedgehog-validit...
Các combinator để kết hợp generator cũng rất tuyệt, và tùy thư viện còn có sẵn tập các giá trị “xấu” đã biết có thể gây ra hành vi ngoại lệ
Lùi lại một bước, tôi muốn đặt câu hỏi mang tính meta hơn về kiểm thử. Test thành công có đồng nghĩa với code thành công, và điều ngược lại có đúng không? Có phần nào trong hợp đồng của Go ghi rõ rằng cùng một input đưa vào cùng một code sẽ cho ra cùng một output không?
Khi làm việc với kiểu Arbitrary biểu diễn tập các đối tượng ngẫu nhiên, bạn có thể dễ dàng viết các hàm tái sử dụng để sinh input kiểm thử. Có vẻ những thư viện như vậy cũng có thể dùng chung khá dễ với framework fuzzing của Go
Dù vậy, tôi thấy các combinator thông thường như map, filter, chain, oneOf có thể hơi gượng, nên đang viết một thư viện kiểm thử thuộc tính mới cho JavaScript. Mục tiêu là làm cho nó dễ dùng hơn, nhưng hiện vẫn mang tính thử nghiệm và chưa công khai
clojure.spec.alphalà một trải nghiệm tuyệt vời, dù có dùng cùngtest.checkhay không, nhưng khi thửhypothesiscủa Python thì thật sự rất tệHypothesis có vẻ như theo thiết kế không xử lý được các tập dữ liệu đơn giản nhưng “lớn”, mà “lớn” ở đây thực ra cũng không lớn đến vậy. [0] Quá đau khổ nên tôi đã gỡ hẳn Hypothesis và kiểm thử dựa trên sinh dữ liệu khỏi bộ kiểm thử Python ở công ty
[0] https://github.com/HypothesisWorks/hypothesis/issues/3493
Hypothesis cố thu nhỏ số nguyên được sinh ra về 0 để xem lỗi có tồn tại cả với 0 không, còn bài test thì từ chối thay vì fail vì có chứa 0. Với các trường hợp nhỏ thì chỉ là kém hiệu quả, nhưng với trường hợp lớn thì đến mức Hypothesis bỏ cuộc
Trong thread đó có người đề xuất dùng một chiến lược sinh instance khác không thể sinh ra 0. Tức là thay vì sinh ra giá trị mà bộ shrinker của Hypothesis thích nhất rồi từ chối nó, thì ngay từ đầu đừng tạo ra nó. Không biết bạn đã thử chưa
Tôi cũng tò mò
clojure.spec.alphaxử lý việc này khác thế nàoBình luận của mjaniczek tại https://news.ycombinator.com/item?id=40876437 nêu trường hợp này như một nhược điểm của cách tiếp cận Hypothesis
Đại ý là: “Vì generator giờ trở thành một parser danh sách byte có thể thất bại, nên sẽ có chút kém hiệu quả, và người dùng có thể tạo ra những generator kỳ lạ mà shrinker nội bộ không thể thu nhỏ hoàn hảo. Dù vậy, trong ba cách tiếp cận thì trải nghiệm developer vẫn là tốt nhất…”
Tất nhiên có lẽ bạn sẽ không đồng ý rằng mình đã viết test theo cách “kỳ lạ”
filtertheo cách tự gây ra vấn đềNếu sinh ngẫu nhiên rồi lọc những thứ phù hợp với một thuộc tính nào đó, thì trong quá trình sinh dữ liệu về cơ bản là bạn đang cào vé số
Câu trả lời ngắn cho câu hỏi trong bài “Tại sao không có yêu cầu rằng nghiên cứu đã xuất bản phải có thể tái lập bằng công cụ mã nguồn mở, hoặc ít nhất là bằng công cụ được cung cấp miễn phí cho công chúng và các nhà nghiên cứu khác?” là hệ quả tức thì của yêu cầu đó sẽ là những bài báo không đáp ứng điều kiện này sẽ không được xuất bản
Chẳng hạn, ngay cả những thứ có vẻ hữu ích cho tác giả và người khác như bài báo về Quviq QuickCheck cũng có thể đã không được xuất bản, và cộng đồng sẽ mất đi món quà thông tin đó
Mọi yêu cầu đều có hiệu ứng loại trừ, và luôn có những trường hợp ranh giới là các bài báo vẫn hữu ích dù không đáp ứng yêu cầu
Nếu coi lập luận này là hợp lệ, ta có thể dùng nó làm lá chắn để đi xa đến bất cứ đâu. Nếu bỏ khả năng tái lập khỏi yêu cầu, thì không cần giải thích bất cứ điều gì mình không muốn giải thích. Không cần cung cấp dữ liệu về mẫu, cũng không cần kiểm định ý nghĩa thống kê. Chỉ một abstract mơ hồ tuyên bố đã đạt được kết quả nào đó là đủ
Thậm chí ghi chú nổi tiếng mà Fermat để lại ở lề bản sao cá nhân của Arithmetica cũng trở thành một bài nghiên cứu hoàn toàn hợp lệ. Vì hẳn ta không muốn mất thông tin quý giá rằng một nhà toán học nổi tiếng nghĩ mình có một chứng minh ngắn gọn và thanh nhã cho một định lý nào đó. Dĩ nhiên trên thực tế rất có thể ông không có
Quan điểm của tôi về câu hỏi chính trị này là tiêu chuẩn hiện nay quá lỏng lẻo. Không ai bị ép buộc phải xuất bản điều gì. Trên đời có nhiều nghiên cứu không được xuất bản ở đâu cả vì những lý do như giá trị độc quyền, và các nghiên cứu đó sẽ không biến mất
Nhưng nếu làm việc trong giới học thuật, hơn nữa còn nhận tài trợ nghiên cứu, và nói rằng mục tiêu là thúc đẩy tri thức khoa học của thế giới tiến lên, thì việc yêu cầu thực sự theo đuổi mục tiêu đó là công bằng. Không nên chỉ giả vờ theo đuổi mục tiêu ấy để leo nấc thang sự nghiệp học thuật
Cung cấp kèm cả những thứ cần thiết để chạy code. Có thể họ đã làm như vậy rồi
Việc có thêm yêu cầu cho mục đích đó khó có khả năng làm giảm số lượng bài báo được xuất bản
Vấn đề nghiêm trọng hơn của các bài báo đã xuất bản là người ta thường cố tình bỏ qua sai sót để xuất bản càng nhiều càng nhanh càng tốt. Nếu việc kiểm chứng bài báo trở nên dễ hơn thì tình hình có thể cải thiện, nhưng tôi sẽ không kỳ vọng quá nhiều. Con người rất giỏi tìm đường tắt
Tôi khá thường xuyên viết kiểm thử thuộc tính có trạng thái bằng
proptestcủa Rust, và thường thì tự code cũng khá đơn giảnMột ví dụ không tầm thường đã tìm ra 6 lỗi nằm ở https://github.com/sunshowers-code/buf-list/blob/main/src/cu...
Kiểm thử song song đôi khi có thể hữu ích, nhưng nhiều lúc chỉ cần chạy nhiều bài test song song lại dễ hơn
Ở tầng trên cùng dùng tính ngẫu nhiên thật, bên dưới đặt nhiều vòng lặp lồng nhau để đi từ các trường hợp độ phức tạp thấp lên các trường hợp độ phức tạp cao. Sau đó tạo và in ra seed để đưa vào bộ sinh số giả ngẫu nhiên tất định. Nếu test thất bại, chỉ cần copy-paste seed lỗi để tái hiện ca thất bại
Tôi thấy kiểu kiểm thử thuộc tính thủ công này nhanh hơn, linh hoạt hơn và nhìn chung ít phiền phức hơn bất kỳ framework hay thư viện nào
Tuy nhiên, với kiểm thử đồng thời thật sự chắc chắn, tôi rất khuyến nghị thư viện AWS Shuttle (https://github.com/awslabs/shuttle). Nó có thể tìm ra những race condition phức tạp đến khó tin. Tôi cũng đã viết một tutorial nhỏ: https://grantslatton.com/shuttle
Ở AWS, chúng tôi đã dùng thư viện này để kiểm chứng hệ thống tệp tùy chỉnh viết để vận hành AWS S3
Tôi đã lướt nhanh bài báo “Testing Telecoms Software with Quviq QuickCheck” được liên kết, nhưng không thấy ngay câu trả lời cho câu hỏi “tại sao không tự tạo các thao tác có trạng thái này thì tốt hơn?”
Bài gốc chỉ vào phần này bằng mô hình các cặp khóa-giá trị của một kho khóa-giá trị, nhưng tôi không hiểu vì sao không thể chỉ viết một state machine, hay vì sao cần framework. Tuần trước ở chỗ làm tôi đã làm đúng như vậy để test tương tác với hệ thống tệp, và cuối cùng nó quy về kiểu
type Instruction = | Read of stuff | Write of stuff | Seek of stuff | …Khi đó thuộc tính sẽ là “với danh sách lệnh này, …”. Kiểu
StateModelvề cơ bản cũng yêu cầu làm cùng việc đó. Tôi khó thấyStateModelthực sự đóng góp phần đáng kể nào; có vẻ nó chỉ giúp giảm một lượng rất nhỏ mã kiểm thử trong thực tế, đổi lại phải thêm vào nhiều mã framework cần hiểu hơn rất nhiềuNếu muốn chỉ sinh các chuỗi chuyển trạng thái “hợp lệ”, thường bạn cần trạng thái mô hình để xác định bước test nào hợp lệ ở một trạng thái cụ thể. Ngoài ra, trong quá trình thu nhỏ, khi loại bỏ các bước test, phải tránh phá vỡ các tiền điều kiện đã được tuân thủ khi sinh từng bước ban đầu, nếu không sẽ tạo ra lỗi giả
Nếu ở trạng thái nào thao tác nào cũng hợp lệ và bạn chỉ muốn một chuỗi thao tác tùy ý hoàn toàn ngẫu nhiên, framework
proptestcó trạng thái có thể là quá mức cần thiết. Nhưng nếu phải duy trì trạng thái mô hình và chỉ định tiền điều kiện cho nhiều thao tác, một framework chuyên dụng sẽ giúp bớt rất nhiều việcNăm ngoái tôi đã viết một bài blog về chủ đề này; nếu muốn xem ví dụ sâu hơn thì có thể tham khảo: https://readyset.io/blog/stateful-property-testing-in-rust
Như những người khác đã nói, kiểm thử state machine song song cũng là một lợi ích hay mà framework chuyên dụng mang lại, nhưng đó không phải lợi ích duy nhất
Có thể trộn các phong cách kiểm thử với nhau. Dù sao cũng là code của bạn
Đó chính là lợi ích
Tác giả tập trung vào khía cạnh máy trạng thái và song song của kiểm thử dựa trên thuộc tính, nhưng cũng có những khía cạnh khác có thể mang lại hiệu quả lớn hơn
Một là kiểm thử dựa trên thuộc tính được dẫn dắt bởi độ bao phủ, có thể xem bài của Dan Luu: https://danluu.com/testing/
Một khía cạnh khác, nơi tôi có phần thiên vị, là tự động hóa việc thu nhỏ trong khi vẫn duy trì mọi bất biến đã được tạo ra khi sinh giá trị
Tóm lại, các hàm thu nhỏ phái sinh kiểu QuickCheck hoạt động trên giá trị (
shrink : a -> [a]) có các ràng buộc và vấn đề, khiến mọi người tắt luôn việc thu nhỏ thay vì xử lý vấn đề“Thu nhỏ tích hợp” bằng cây rose (ví dụ: Hedgehog) tuân theo các ràng buộc của generator, nhưng gặp vấn đề với monad bind, tức là trường hợp dùng kết quả của generator để rẽ nhánh sang generator khác
Cách tiếp cận duy nhất trông như có phép màu “cứ thế mà chạy” là thu nhỏ nội bộ của Hypothesis. Nó dùng một tầng gián tiếp để rút gọn danh sách các lựa chọn ngẫu nhiên thay vì chính giá trị. Nhược điểm là generator giờ trở thành một parser danh sách byte có thể thất bại, gây ra một chút kém hiệu quả, và người dùng có thể tạo ra những generator kỳ lạ mà bộ thu nhỏ nội bộ không thể rút gọn hoàn hảo. Dù vậy, trong ba cách tiếp cận, đây là cách có trải nghiệm nhà phát triển tốt nhất; xét đến việc chỉ riêng chuyện mọi người chịu viết test đã là một phép màu nhỏ, với tư cách tác giả thư viện kiểm thử, tôi thấy đây là cách đáng xây dựng nhất
Thay vào đó, để thu nhỏ tối ưu nên ưu tiên generator applicative: https://github.com/hedgehogqa/haskell-hedgehog/issues/473#is...
Nói cách khác, generator applicative không “dùng kết quả của generator để rẽ nhánh sang generator khác”, và nhờ tính chất “song song” của applicative mà việc thu nhỏ được tối ưu hóa. Ở đây “song song” không mang nghĩa threading trong bài viết, mà theo nghĩa monadic. Vì applicative là “song song” nên các generator có thể được rút gọn độc lập. Ngược lại, generator monad là “tuần tự”, nên khi rút gọn một cái thì hành vi của generator theo sau nhất thiết sẽ thay đổi
Nếu có bài trình bày công khai thì tôi muốn xem liên kết
Tôi không nghĩ nó thực sự sẵn sàng cho production, và điều đó có vẻ là do thiết kế.[0]
Tôi đã dùng
clojure.spec.alphakhá nhiều, cả cùng vớitest.checklẫn không, nên dù có khác biệt, tôi cũng không hoàn toàn xa lạ với ý tưởng chung[0] https://github.com/HypothesisWorks/hypothesis/issues/3493
Nó giống Hypothesis nhưng dùng cây generator thay vì chuỗi tuyến tính. Nó dựa trên selective functor, một giao diện tốt và cũng hữu ích cho những thứ như validator
Theo https://hackage.haskell.org/package/falsify, thư viện này cung cấp kiểm thử dựa trên thuộc tính hỗ trợ thu nhỏ tích hợp nội bộ. “Tích hợp” theo nghĩa của Hedgehog, tức là không cần viết riêng shrinker và generator; và “nội bộ” theo nghĩa của Hypothesis, tức là vẫn hoạt động tốt xuyên suốt monad bind
Tôi từng thử dùng kiểm thử dựa trên thuộc tính nhưng luôn có cảm giác bị kẹt giữa hai chiếc ghế
Nếu hiểu một thuộc tính đủ rõ để kiểm thử nghiêm ngặt, thường thì có thể đẩy nó vào hệ thống kiểu để nó đúng theo cấu trúc. Còn nếu chỉ muốn một bài smoke test đơn giản thì một input tùy ý sẽ dễ hơn
Ví dụ, thường có hai implementation: một bản ngây thơ chậm nhưng đơn giản và một bản đã tối ưu, rồi có thể so sánh output của chúng với input tùy ý. Đây là một thuộc tính đơn giản, dễ hiểu, nhưng nói chung khó đưa vào hệ thống kiểu
Tương tự, có thể có các tính chất như thứ tự đưa input vào không được quan trọng, hoặc có cách chia dữ liệu sao cho
max(giá trị lớn nhất của A, giá trị lớn nhất của B) = maximum(A union B). Làm sao encode những thứ như vậy vào hệ thống kiểu?Hoặc những điều như “với A và B bất kỳ, một nghiệm tối ưu tìm được trong A sẽ không tệ hơn một nghiệm tối ưu nào đó tìm được trong A union B”, hay tính lũy đẳng như
f(f(A)) = f(A)Tất cả đều là những thuộc tính dễ hiểu, nhưng không dễ mô tả trong hầu hết các hệ thống kiểu
Nhưng có nhiều ràng buộc mà các trình kiểm tra kiểu phổ biến không xử lý được. Kiểu phụ thuộc sẽ giúp rất nhiều, nhưng hiện có vẻ vẫn bị giới hạn trong các ngách như theorem prover
Tôi tự hỏi liệu danh sách có đang thiếu QuviQ Erlang QuickCheck gốc không
Sản phẩm đầy đủ là độc quyền, nhưng họ cũng cung cấp bản miễn phí QuickCheck Mini: http://www.quviq.com/downloads/
Clojure giờ cũng có thư viện quickcheck có trạng thái: https://github.com/griffinbank/test.contract
Kiểm thử song song thì thú vị, nhưng đến giờ vẫn chưa phải nguồn gây đau đầu lớn
Với các bài test C#/.NET, tôi đã dùng CsCheck[0] và khá hài lòng
Nó dễ tiếp cận hơn nhiều so với Hedgehog hay FsCheck, và tốc độ cũng khá nhanh
[0] https://github.com/AnthonyLloyd/CsCheck
Nó cũng hỗ trợ các bài test tuyến tính hóa được/song song như bài viết mô tả
Tham khảo:
https://github.com/AnthonyLloyd/CsCheck?tab=readme-ov-file#m...
https://github.com/AnthonyLloyd/CsCheck?tab=readme-ov-file#c...
Việc có một biến thể C# riêng có vẻ là hợp lý